camera.vue 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618
  1. <template>
  2. <view class="camera-container">
  3. <!-- 顶部相机区域 - 固定位置 -->
  4. <view class="fixed-camera-container">
  5. <view class="user-avatar">
  6. <camera v-if="!digitalHumanUrl" device-position="front" flash="auto" class="camera"
  7. :mode="mode" @error="handleCameraError"></camera>
  8. <web-view v-else-if="digitalHumanUrl" :src="digitalHumanUrl" class="digital-human-webview"></web-view>
  9. <image v-else src="/static/avatar.png" mode="aspectFill" class="avatar-image"></image>
  10. </view>
  11. </view>
  12. <!-- 主要内容区域 - 修改为流式布局 -->
  13. <scroll-view class="content-scroll" scroll-y="true" :scroll-into-view="currentScrollId" :scroll-with-animation="true" :scroll-top="scrollTop">
  14. <view class="content">
  15. <!-- 测试内容区域 - 修改为流式布局,每个问题都显示 -->
  16. <view v-for="(question, qIndex) in questions" :key="question.id" :id="'question-'+qIndex" class="interview-content">
  17. <!-- 问题编号和进度 -->
  18. <view class="question-number">
  19. <text>{{qIndex + 1}}/{{questions.length}}</text>
  20. </view>
  21. <!-- 问题和选项区域 -->
  22. <view class="question-area">
  23. <view class="question-importance">
  24. <!-- <text v-if="currentQuestion.isImportant">重点题</text> -->
  25. <text class="question-type">{{question.questionTypeName}}</text>
  26. {{question.text}}<text class="question-type" v-if="question.is_required_correct">(红线题)</text>
  27. </view>
  28. <!-- 添加图片显示区域,仅在 question_form 为 3 时显示 -->
  29. <view class="question-image-container" v-if="question.questionType === 3 && question.imageUrl">
  30. <image :src="question.imageUrl" mode="widthFix" class="question-image"></image>
  31. </view>
  32. <view class="options">
  33. <!-- 渲染非开放问题 (questionType: 1, 2 或 3) -->
  34. <view
  35. v-if="question.questionType !== 0"
  36. v-for="(option, index) in question.options"
  37. :key="index"
  38. class="option-item"
  39. :class="{
  40. 'option-selected': isOptionSelected(question, qIndex, index),
  41. 'option-disabled': qIndex !== currentQuestionIndex
  42. }"
  43. @click="handleOptionClick(qIndex, index)">
  44. <text class="option-text">{{ option.option_text || (typeof option === 'string' ? option : JSON.stringify(option)) }}</text>
  45. </view>
  46. </view>
  47. <!-- 添加当前题目的按钮 - 只在多选题时显示 -->
  48. <view class="question-button-container" v-if="qIndex === currentQuestionIndex && question.questionType === 2">
  49. <button class="next-button" @click="nextQuestion()"
  50. :disabled="selectedOptions.length === 0">
  51. {{"提交答案"}}
  52. </button>
  53. </view>
  54. </view>
  55. </view>
  56. <view class="continue-button-container" v-if="showContinueButton" id="continue-button">
  57. <button class="continue-button" @click="handleContinue">
  58. {{ currentGroupIndex < questionGroups.length - 1 ? '继续下一部分' : '完成测试' }}
  59. </button>
  60. </view>
  61. <!-- 添加底部锚点元素 -->
  62. <view id="bottom-anchor" style="height: 1px;"></view>
  63. <!-- 添加底部空白区域,确保内容不被固定按钮遮挡 -->
  64. <view class="bottom-space"></view>
  65. </view>
  66. </scroll-view>
  67. <!-- 面试结束弹窗 -->
  68. <view class="interview-end-modal" v-if="showEndModal">
  69. <view class="modal-content">
  70. <view class="modal-title">测试已完成</view>
  71. <!-- <view class="score-display">{{score}}/{{totalQuestions}}</view>
  72. <view class="modal-message">您在本次测试中答了{{score}}道题目,共{{totalQuestions}}道题目。</view> -->
  73. <view class="modal-buttons">
  74. <!-- <button type="default" class="modal-button" @click="restartTest">重新测试</button> -->
  75. <button type="primary" class="modal-button" @click="navigateToInterview">进入面试</button>
  76. </view>
  77. </view>
  78. </view>
  79. <!-- AI面试结束页面 -->
  80. <view class="interview-complete-screen" v-if="interviewCompleted">
  81. <view class="digital-avatar-large">
  82. <image src="/static/avatar.png" mode="aspectFill" class="avatar-image-large"></image>
  83. </view>
  84. <view class="complete-message">AI面试已结束</view>
  85. <view class="complete-description">本次面试的全部环节已结束。非常感谢您的参与。</view>
  86. <button class="complete-button" @click="back">我知道了</button>
  87. </view>
  88. <!-- 在模板中添加一个测试按钮(开发时使用,发布前删除) -->
  89. <!-- <button @click="testEndScreen" style="position: absolute; top: 10px; right: 10px; z-index: 9999;">测试结束页</button> -->
  90. <!-- 添加加载状态 -->
  91. <view class="loading-container" v-if="loading">
  92. <uni-load-more status="loading" :contentText="{contentdown: '加载中...'}" />
  93. <text class="loading-text">正在加载面试题目...</text>
  94. </view>
  95. <!-- 添加加载错误提示 -->
  96. <view class="loading-container" v-if="!loading && loadError">
  97. <view class="error-container">
  98. <text class="error-message">加载面试题目失败</text>
  99. <button class="retry-button" @click="fetchInterviewData">重试</button>
  100. </view>
  101. </view>
  102. </view>
  103. </template>
  104. <script>
  105. import { getInterviewList, getInterviewDetail,getQuestions } from '@/api/user.js';
  106. import { apiBaseUrl } from '@/common/config.js';
  107. export default {
  108. data() {
  109. return {
  110. cameraContext: null,
  111. devicePosition: 'front',
  112. interviewStarted: true,
  113. showEndModal: false,
  114. currentQuestionIndex: 0,
  115. currentStep: 1,
  116. stepTexts: ['测试准备', '回答问题', '测试结束'],
  117. progressWidth: 50,
  118. remainingTime: '00:27',
  119. selectedOption: null,
  120. selectedOptions: [],
  121. showResult: false,
  122. isAnswerCorrect: false,
  123. questions: [], // 改为空数组,将通过API获取
  124. interviewId: null, // 存储当前面试ID
  125. useVideo: false,
  126. timerInterval: null,
  127. score: 0,
  128. totalQuestions: 0,
  129. interviewCompleted: false,
  130. digitalHumanUrl: '', // 数字人URL
  131. loading: true, // 添加加载状态
  132. loadError: false, // 添加加载错误状态
  133. errorMessage: '', // 添加错误消息
  134. answers: [], // 存储用户的所有答案
  135. currentQuestionDetail: null, // 当前题目详情
  136. isSubmitting: false, // 是否正在提交答案
  137. openQuestionAnswer: '', // 存储开放问题的答案
  138. currentAnswer: null, // 存储当前答案以便提交
  139. questionStartTime: null, // 存储问题开始时间
  140. mode: 'normal', // 相机模式
  141. currentScrollId: 'question-0', // 当前滚动到的问题ID
  142. questionGroups: [], // 存储分组后的题目
  143. currentGroupIndex: 0, // 当前显示的组索引
  144. showContinueButton: false, // 是否显示继续按钮
  145. scrollToBottom: false, // 新增:是否需要滚动到底部
  146. scrollTop: 0, // 控制 scroll-view 的滚动位置
  147. }
  148. },
  149. computed: {
  150. currentQuestion() {
  151. console.log(this.questions[this.currentQuestionIndex]);
  152. return this.questions[this.currentQuestionIndex];
  153. }
  154. },
  155. onLoad(options) {
  156. // 从路由参数中获取面试ID
  157. if (options && options.id) {
  158. this.interviewId = options.id;
  159. this.fetchInterviewData();
  160. } else {
  161. // 如果没有ID,可以获取面试列表并使用第一个
  162. this.fetchInterviewList();
  163. }
  164. },
  165. onReady() {
  166. // 创建相机上下文
  167. this.cameraContext = uni.createCameraContext();
  168. // 创建AI视频上下文
  169. if (this.useVideo) {
  170. this.aiVideoContext = uni.createVideoContext('aiInterviewer');
  171. }
  172. // 初始化数字人
  173. this.initDigitalHuman();
  174. },
  175. methods: {
  176. // 获取面试列表
  177. async fetchInterviewList() {
  178. try {
  179. this.loading = true;
  180. const res = await getQuestions({
  181. position_id:JSON.parse(uni.getStorageSync('selectedJob')).id,
  182. page:1,
  183. limit:999,
  184. tenant_id:JSON.parse(uni.getStorageSync('userInfo')).tenant_id || 1
  185. });
  186. console.log(res.items);
  187. // 使用第一个面试
  188. this.interviewId = res.items//[0].id;
  189. this.fetchInterviewData(res.items);
  190. } catch (error) {
  191. console.error('获取面试列表失败:', error);
  192. this.handleLoadError('获取面试列表失败');
  193. }
  194. },
  195. // 获取面试详情数据
  196. async fetchInterviewData(data) {
  197. try {
  198. this.loading = true;
  199. // const res = await getInterviewDetail({ id: this.interviewId });
  200. // console.log('API返回数据:', res);
  201. // 如果返回的是问题列表
  202. if (data && Array.isArray(data)) {
  203. const formattedQuestions = data
  204. .filter(q => q.question_form !== 0)
  205. .map((q, index) => ({
  206. id: q.id || index + 1,
  207. text: q.question || '未知问题',
  208. options: q.options || [],
  209. correctAnswer: q.correctAnswer || 0,
  210. isImportant: q.is_system || false,
  211. explanation: q.explanation || '',
  212. questionType: q.question_form,
  213. questionTypeName: q.question_form_name || '单选题',
  214. is_required_correct: q.is_required_correct || false,
  215. correctAnswers: q.correct_answers || [],
  216. difficulty: q.difficulty || 1,
  217. difficultyName: q.difficulty_name || '初级',
  218. imageUrl: q.question_image_url || '', // 添加图片URL字段
  219. category: q.category || 0, // 添加 category 字段
  220. }));
  221. // 处理题目分组
  222. this.processQuestionGroups(formattedQuestions);
  223. } else {
  224. // 处理单个问题
  225. this.processInterviewData(res);
  226. }
  227. console.log(this.questions);
  228. // 设置总题目数
  229. this.totalQuestions = this.questions.length;
  230. /* // 启动倒计时
  231. if (this.questions.length > 0) {
  232. this.startTimer();
  233. } */
  234. } catch (error) {
  235. console.error('获取面试详情失败:', error);
  236. this.handleLoadError('获取面试详情失败');
  237. } finally {
  238. this.loading = false;
  239. }
  240. },
  241. // 处理面试数据
  242. processInterviewData(data) {
  243. // 清空现有问题列表
  244. this.questions = [];
  245. // 检查数据格式并处理
  246. if (data && data.question_form !== 0) { // 只处理非开放问题
  247. // 创建一个格式化的问题对象
  248. const formattedQuestion = {
  249. id: data.id || 1,
  250. text: data.question || '未知问题',
  251. options: data.options || [],
  252. correctAnswer: data.correctAnswer || 0,
  253. isImportant: data.is_system || false,
  254. explanation: data.explanation || '',
  255. questionType: data.question_form, // 1-单选题,2-多选题,3-看图答题
  256. questionTypeName: data.question_form_name || '单选题',
  257. is_required_correct: data.is_required_correct || false,
  258. correctAnswers: data.correct_answers || [],
  259. difficulty: data.difficulty || 1,
  260. difficultyName: data.difficulty_name || '初级',
  261. imageUrl: data.question_image_url || '' // 添加图片URL字段
  262. };
  263. // 添加到问题列表
  264. this.questions.push(formattedQuestion);
  265. // 设置总题目数
  266. this.totalQuestions = this.questions.length;
  267. /* // 启动倒计时
  268. this.startTimer(); */
  269. } else {
  270. this.handleLoadError('没有可用的选择题');
  271. }
  272. },
  273. // 处理加载错误
  274. handleLoadError(message) {
  275. this.loadError = true;
  276. this.loading = false;
  277. this.errorMessage = message || '加载失败';
  278. uni.showToast({
  279. title: message || '加载失败',
  280. icon: 'none',
  281. duration: 2000
  282. });
  283. },
  284. startTimer() {
  285. // 确保问题已加载
  286. if (this.questions.length === 0) return;
  287. // 记录开始时间
  288. this.questionStartTime = new Date();
  289. // 设置倒计时时间(秒)
  290. let seconds = 60; // 每题60秒,可以根据需要调整
  291. this.remainingTime = `01:00`;
  292. this.timerInterval = setInterval(() => {
  293. seconds--;
  294. if (seconds <= 0) {
  295. clearInterval(this.timerInterval);
  296. // 时间到,自动提交答案并进入下一题
  297. if (!this.showResult) {
  298. this.checkAnswer();
  299. // 短暂延迟后自动进入下一题
  300. setTimeout(() => {
  301. if (this.currentQuestionIndex < this.questions.length - 1) {
  302. this.goToNextQuestion();
  303. } else {
  304. // 如果是最后一题,显示结束模态框或跳转
  305. uni.navigateTo({
  306. url: '/pages/interview-question/interview-question'
  307. });
  308. }
  309. }, 1500);
  310. }
  311. }
  312. // 格式化显示时间 (不再显示,但仍然计算用于内部逻辑)
  313. const min = Math.floor(seconds / 60).toString().padStart(2, '0');
  314. const sec = (seconds % 60).toString().padStart(2, '0');
  315. this.remainingTime = `${min}:${sec}`;
  316. }, 1000);
  317. },
  318. resetTimer() {
  319. clearInterval(this.timerInterval);
  320. /* this.startTimer(); */
  321. },
  322. selectOption(index) {
  323. // 如果是最后一题且已经显示继续按钮,不允许重复作答
  324. if (this.currentQuestionIndex === this.questions.length - 1 && this.showContinueButton) {
  325. uni.showToast({
  326. title: '该题目已完成,请继续下一部分',
  327. icon: 'none',
  328. duration: 2000
  329. });
  330. return;
  331. }
  332. // 如果已经显示结果,不允许再次选择
  333. if (this.showResult) return;
  334. console.log('selectOption', index);
  335. // 判断当前题目类型
  336. if (this.currentQuestion.questionType === 2) {
  337. // 如果已经选中,则取消选中
  338. const optionIndex = this.selectedOptions.indexOf(index);
  339. if (optionIndex > -1) {
  340. this.selectedOptions.splice(optionIndex, 1);
  341. } else {
  342. // 否则添加到已选中数组
  343. this.selectedOptions.push(index);
  344. }
  345. // 多选题不自动提交,需要用户手动点击"提交答案"按钮
  346. } else if (this.currentQuestion.questionType === 1 || this.currentQuestion.questionType === 3 || this.currentQuestion.questionType === 4) {
  347. this.selectedOption = index;
  348. this.playAiSpeaking();
  349. setTimeout(() => {
  350. this.checkAnswer();
  351. this.saveAnswer();
  352. this.submitCurrentAnswer().then(() => {
  353. setTimeout(() => {
  354. this.goToNextQuestion();
  355. }, 800);
  356. }).catch(error => {
  357. console.error('提交答案失败:', error);
  358. setTimeout(() => {
  359. this.goToNextQuestion();
  360. }, 1000);
  361. });
  362. }, 500);
  363. } else {
  364. // 单选题逻辑
  365. this.selectedOption = index;
  366. // 保存答案到 answers 数组
  367. const answer = {
  368. questionId: this.currentQuestion.id,
  369. questionType: this.currentQuestion.questionType,
  370. answer: index
  371. };
  372. const existingIndex = this.answers.findIndex(a => a.questionId === answer.questionId);
  373. if (existingIndex > -1) {
  374. this.answers[existingIndex] = answer;
  375. } else {
  376. this.answers.push(answer);
  377. }
  378. this.playAiSpeaking();
  379. setTimeout(() => {
  380. this.checkAnswer();
  381. this.saveAnswer();
  382. this.submitCurrentAnswer().then(() => {
  383. setTimeout(() => {
  384. this.goToNextQuestion();
  385. }, 800);
  386. }).catch(error => {
  387. console.error('提交答案失败:', error);
  388. setTimeout(() => {
  389. this.goToNextQuestion();
  390. }, 1000);
  391. });
  392. }, 500);
  393. }
  394. },
  395. checkAnswer() {
  396. // 如果已经显示结果,说明已经提交过答案,不再重复提交
  397. if (this.showResult) return;
  398. clearInterval(this.timerInterval); // 停止计时
  399. // 确保当前问题存在
  400. if (!this.currentQuestion) {
  401. console.error('当前问题不存在');
  402. return;
  403. }
  404. // 根据题目类型检查答案是否正确
  405. if (this.currentQuestion.questionType === 0) {
  406. this.isAnswerCorrect = true;
  407. } else if (this.currentQuestion.questionType === 2) {
  408. const sortedSelected = [...this.selectedOptions].sort();
  409. const sortedCorrect = [...this.currentQuestion.correctAnswers].sort();
  410. if (sortedSelected.length !== sortedCorrect.length) {
  411. this.isAnswerCorrect = false;
  412. } else {
  413. this.isAnswerCorrect = sortedSelected.every((value, index) => value === sortedCorrect[index]);
  414. }
  415. } else {
  416. this.isAnswerCorrect = this.selectedOption === this.currentQuestion.correctAnswer;
  417. }
  418. // 显示结果
  419. this.showResult = true;
  420. // 保存当前题目的答案
  421. this.saveAnswer();
  422. // 如果是最后一题,确保状态被保存
  423. if (this.currentQuestionIndex === this.questions.length - 1) {
  424. this.$nextTick(() => {
  425. // 强制更新视图
  426. this.$forceUpdate();
  427. });
  428. }
  429. },
  430. // 修改 nextQuestion 方法,只处理多选题
  431. nextQuestion() {
  432. // 确保当前是多选题
  433. if (this.currentQuestion.questionType !== 2) return;
  434. // 检查答案并提交
  435. this.checkAnswer();
  436. // 保存当前题目的答案
  437. this.saveAnswer();
  438. // 提交答案
  439. this.submitCurrentAnswer().then(() => {
  440. // 提交成功后,短暂延迟后自动进入下一题
  441. setTimeout(() => {
  442. // 如果是最后一题,跳转到面试页面
  443. if (this.currentQuestionIndex >= this.questions.length - 1) {
  444. uni.navigateTo({
  445. url: '/pages/interview-question/interview-question'
  446. });
  447. } else {
  448. // 否则进入下一题
  449. this.goToNextQuestion();
  450. }
  451. }, 500); // 0.5秒后自动进入下一题
  452. }).catch(error => {
  453. console.error('提交答案失败:', error);
  454. // 即使提交失败也继续下一题
  455. setTimeout(() => {
  456. if (this.currentQuestionIndex >= this.questions.length - 1) {
  457. uni.navigateTo({
  458. url: '/pages/interview-question/interview-question'
  459. });
  460. } else {
  461. this.goToNextQuestion();
  462. }
  463. }, 1000);
  464. });
  465. },
  466. // 保存当前题目的答案
  467. saveAnswer() {
  468. let answer;
  469. if (this.currentQuestion.questionType === 0) { // 开放问题
  470. answer = {
  471. questionId: this.currentQuestion.id,
  472. questionType: this.currentQuestion.questionType,
  473. answer: this.openQuestionAnswer,
  474. answerDuration: this.getAnswerDuration() // 添加答题时长
  475. };
  476. } else if (this.currentQuestion.questionType === 1 || this.currentQuestion.questionType === 3 || this.currentQuestion.questionType === 4) { // 单选题、看图答题或类型4题目
  477. answer = {
  478. questionId: this.currentQuestion.id,
  479. questionType: this.currentQuestion.questionType,
  480. answer: this.selectedOption,
  481. answerDuration: this.getAnswerDuration() // 添加答题时长
  482. };
  483. } else { // 多选题
  484. answer = {
  485. questionId: this.currentQuestion.id,
  486. questionType: this.currentQuestion.questionType,
  487. answer: this.selectedOptions,
  488. answerDuration: this.getAnswerDuration() // 添加答题时长
  489. };
  490. }
  491. // 检查是否已存在该题目的答案,如果存在则更新
  492. const existingIndex = this.answers.findIndex(a => a.questionId === answer.questionId);
  493. if (existingIndex > -1) {
  494. this.answers[existingIndex] = answer;
  495. } else {
  496. this.answers.push(answer);
  497. }
  498. // 保存当前答案以便提交
  499. this.currentAnswer = answer;
  500. console.log('已保存答案:', this.answers);
  501. },
  502. // 获取答题时长(秒)
  503. getAnswerDuration() {
  504. // 这里假设每题默认30秒,根据剩余时间计算用时
  505. // 您可以根据实际情况调整计算逻辑
  506. const remainingTimeArr = this.remainingTime.split(':');
  507. const remainingSeconds = parseInt(remainingTimeArr[0]) * 60 + parseInt(remainingTimeArr[1]);
  508. return 30 - remainingSeconds; // 假设每题30秒
  509. },
  510. // 提交当前答案
  511. async submitCurrentAnswer() {
  512. if (!this.currentAnswer || this.isSubmitting) return; // 如果正在提交或没有答案,则不执行
  513. try {
  514. // 设置提交状态为true
  515. this.isSubmitting = true;
  516. // 显示提交中提示
  517. uni.showLoading({
  518. title: '正在提交答案...'
  519. });
  520. // 构建提交数据
  521. let answerContent = '';
  522. let answerOptions = [];
  523. if (this.currentAnswer.questionType === 0) {
  524. // 开放题直接使用文本答案
  525. answerContent = this.currentAnswer.answer;
  526. answerOptions = [];
  527. } else if (this.currentAnswer.questionType === 1 || this.currentAnswer.questionType === 3 || this.currentAnswer.questionType === 4) {
  528. // 单选题或看图答题,获取选项ID而不是索引
  529. const selectedIndex = this.currentAnswer.answer;
  530. const selectedOption = this.currentQuestion.options[selectedIndex];
  531. console.log('selectedOption',selectedOption);
  532. const optionId = selectedOption.id ? selectedOption.id : selectedIndex;
  533. answerContent = optionId.toString();
  534. answerOptions = [optionId];
  535. } else if (this.currentAnswer.questionType === 2) {
  536. // 多选题,将选项ID数组转为逗号分隔的字符串
  537. const selectedIndices = this.currentAnswer.answer;
  538. const selectedOptionIds = selectedIndices.map(index => {
  539. const option = this.currentQuestion.options[index];
  540. // 使用选项的ID或其他唯一标识符
  541. return option.id ? option.id : index;
  542. });
  543. answerOptions = selectedOptionIds;
  544. }
  545. const submitData = {
  546. position_id: JSON.parse(uni.getStorageSync('selectedJob')).id,
  547. applicant_id: JSON.parse(uni.getStorageSync('userInfo')).id,
  548. job_position_question_id: this.currentAnswer.questionId,
  549. // answer_content: answerContent,
  550. answer_options: answerOptions,
  551. answer_duration: this.currentAnswer.answerDuration || 0,
  552. tenant_id:JSON.parse(uni.getStorageSync('userInfo')).tenant_id || 1
  553. };
  554. console.log('提交数据:', submitData);
  555. // 调用API提交答案
  556. const res = await this.$http.post(`${apiBaseUrl}/api/job/submit_answer`, submitData);
  557. console.log('提交答案响应:', res);
  558. return res;
  559. } catch (error) {
  560. console.error('提交答案失败:', error);
  561. // 显示错误提示
  562. uni.showToast({
  563. title: '提交答案失败,请重试',
  564. icon: 'none'
  565. });
  566. throw error;
  567. } finally {
  568. // 隐藏加载提示
  569. uni.hideLoading();
  570. // 重置提交状态
  571. this.isSubmitting = false;
  572. }
  573. },
  574. // 修改 goToNextQuestion 方法
  575. async goToNextQuestion() {
  576. // 检查是否完成当前组的所有题目
  577. if (this.currentQuestionIndex >= this.questions.length - 1) {
  578. // 当前组的所有题目都已完成,显示继续按钮
  579. this.showContinueButton = true;
  580. // 使用延时确保按钮渲染完成后再滚动
  581. setTimeout(() => {
  582. // 先重置 scrollTop,然后再设置新值,强制触发更新
  583. this.scrollTop = 0;
  584. // 使用 nextTick 确保视图已更新
  585. this.$nextTick(() => {
  586. // 延迟设置新的滚动位置
  587. setTimeout(() => {
  588. this.scrollTop = 10000; // 一个足够大的值
  589. }, 100);
  590. });
  591. }, 200);
  592. return;
  593. }
  594. // 如果还有下一题,继续下一题
  595. this.currentQuestionIndex++;
  596. this.currentScrollId = 'question-' + this.currentQuestionIndex;
  597. // 重置状态(只在非最后一题时重置)
  598. this.showResult = false;
  599. this.selectedOption = null;
  600. this.selectedOptions = [];
  601. this.openQuestionAnswer = '';
  602. // 如果有下一题的详情
  603. if (this.questions[this.currentQuestionIndex]) {
  604. // 更新进度
  605. this.progressWidth = (this.currentQuestionIndex + 1) / this.questions.length * 100;
  606. // 重置计时器
  607. this.resetTimer();
  608. // 播放AI介绍下一题
  609. this.playAiSpeaking();
  610. setTimeout(() => {
  611. this.pauseAiSpeaking();
  612. }, 2000);
  613. }
  614. },
  615. toggleSettings() {
  616. // 打开设置面板
  617. uni.showToast({
  618. title: '设置功能开发中',
  619. icon: 'none'
  620. });
  621. },
  622. back(target) {
  623. // 清除计时器
  624. if (this.timerInterval) {
  625. clearInterval(this.timerInterval);
  626. }
  627. // 如果指定了跳转目标
  628. if (target) {
  629. uni.navigateTo({
  630. url: target
  631. });
  632. return;
  633. }
  634. // 否则尝试返回上一页,如果失败则跳转到首页
  635. // try {
  636. // const pages = getCurrentPages();
  637. // if (pages.length > 1) {
  638. // uni.reLaunch({
  639. // url: '/pages/index/index'
  640. // });
  641. // } else {
  642. // // 如果当前是第一页,则跳转到首页
  643. // uni.reLaunch({
  644. // url: '/pages/index/index'
  645. // });
  646. // }
  647. // } catch (e) {
  648. // console.error('导航错误:', e);
  649. // // 出错时也跳转到首页
  650. // uni.reLaunch({
  651. // url: '/pages/index/index'
  652. // });
  653. // }
  654. },
  655. error(e) {
  656. // 相机错误事件
  657. console.error(e.detail);
  658. uni.showToast({
  659. title: '相机启动失败,请检查权限设置',
  660. icon: 'none'
  661. });
  662. },
  663. playAiSpeaking() {
  664. if (this.useVideo && this.aiVideoContext) {
  665. this.aiVideoContext.play();
  666. }
  667. // 触发数字人说话动画
  668. if (this.digitalHumanUrl) {
  669. // 假设当前问题的文本作为数字人要说的内容
  670. const speakText = this.currentQuestion ? this.currentQuestion.text : '';
  671. this.interactWithDigitalHuman(speakText);
  672. }
  673. },
  674. pauseAiSpeaking() {
  675. if (this.useVideo && this.aiVideoContext) {
  676. this.aiVideoContext.pause();
  677. }
  678. },
  679. // 修改 restartTest 方法,添加可选的跳转目标
  680. restartTest(target) {
  681. this.currentQuestionIndex = 0;
  682. this.score = 0;
  683. this.showEndModal = false;
  684. this.showResult = false;
  685. this.selectedOption = null;
  686. this.selectedOptions = [];
  687. this.resetTimer();
  688. // 如果指定了跳转目标
  689. if (target) {
  690. uni.navigateTo({
  691. url: target
  692. });
  693. }
  694. },
  695. // 在methods中添加测试方法
  696. testEndScreen() {
  697. this.interviewCompleted = true;
  698. this.showEndModal = false;
  699. },
  700. // 初始化数字人
  701. initDigitalHuman() {
  702. // 移除不可用的示例URL
  703. // this.digitalHumanUrl = 'https://your-digital-human-service.com/avatar?id=123';
  704. // 暂时不使用数字人服务,直接使用本地图片
  705. this.digitalHumanUrl = '';
  706. // 如果您有实际可用的数字人服务,可以在这里设置
  707. // 例如:this.digitalHumanUrl = 'https://your-actual-service.com/avatar';
  708. // 或者使用本地HTML文件(如果有的话)
  709. // this.digitalHumanUrl = '/hybrid/html/digital-human.html';
  710. },
  711. // 与数字人交互的方法
  712. interactWithDigitalHuman(message) {
  713. // 如果数字人是通过web-view加载的,可以使用postMessage与其通信
  714. const webview = this.$mp.page.$getAppWebview().children()[0];
  715. if (webview) {
  716. webview.evalJS(`receiveMessage('${message}')`);
  717. }
  718. },
  719. // 添加 navigateToInterview 方法
  720. navigateToInterview() {
  721. // 关闭模态框
  722. this.showEndModal = false;
  723. // 跳转到interview页面
  724. uni.navigateTo({
  725. url: '/pages/interview/interview',
  726. success: () => {
  727. console.log('成功跳转到interview页面');
  728. },
  729. fail: (err) => {
  730. console.error('跳转失败:', err);
  731. uni.showToast({
  732. title: '跳转失败,请手动返回首页',
  733. icon: 'none'
  734. });
  735. }
  736. });
  737. },
  738. // 添加 handleCameraError 方法
  739. handleCameraError(e) {
  740. console.error('相机错误:', e.detail);
  741. uni.showToast({
  742. title: '相机启动失败,请检查权限设置',
  743. icon: 'none'
  744. });
  745. },
  746. // 修改 isOptionSelected 方法
  747. isOptionSelected(question, qIndex, optionIndex) {
  748. // 获取该题的答案
  749. const answer = this.answers.find(a => a.questionId === question.id);
  750. // 如果是当前正在回答的题目
  751. if (qIndex === this.currentQuestionIndex) {
  752. if (question.questionType === 1 || question.questionType === 3 || question.questionType === 4) {
  753. return this.selectedOption === optionIndex;
  754. } else if (question.questionType === 2) {
  755. return this.selectedOptions.includes(optionIndex);
  756. }
  757. }
  758. // 如果是已回答过的题目,使用保存的答案
  759. else if (answer) {
  760. if (question.questionType === 1 || question.questionType === 3 || question.questionType === 4) {
  761. return answer.answer === optionIndex;
  762. } else if (question.questionType === 2 && Array.isArray(answer.answer)) {
  763. return answer.answer.includes(optionIndex);
  764. }
  765. }
  766. return false;
  767. },
  768. // 修改 handleOptionClick 方法
  769. handleOptionClick(qIndex, index) {
  770. // 如果点击的是已完成的题目
  771. if (qIndex < this.currentQuestionIndex) {
  772. uni.showToast({
  773. title: '该题目已完成,无法修改',
  774. icon: 'none',
  775. duration: 2000
  776. });
  777. return;
  778. }
  779. // 如果是最后一题且已经显示继续按钮,不允许重复作答
  780. if (qIndex === this.questions.length - 1 && this.showContinueButton) {
  781. uni.showToast({
  782. title: '该题目已完成,请继续下一部分',
  783. icon: 'none',
  784. duration: 2000
  785. });
  786. return;
  787. }
  788. // 如果点击的是还未到的题目
  789. if (qIndex > this.currentQuestionIndex) {
  790. // 查找第一个未完成的题目索引
  791. const firstUnansweredIndex = this.currentQuestionIndex;
  792. uni.showToast({
  793. title: `请先完成第 ${firstUnansweredIndex + 1} 题`,
  794. icon: 'none',
  795. duration: 2000
  796. });
  797. // 滚动到当前应该做的题目
  798. this.currentScrollId = 'question-' + firstUnansweredIndex;
  799. return;
  800. }
  801. // 如果是当前题目,调用原有的选择逻辑
  802. this.selectOption(index);
  803. },
  804. // 处理题目分组
  805. processQuestionGroups(questions) {
  806. // 创建三个分组
  807. const group1 = []; // category 为 10 或 7 的题目
  808. const group2 = []; // category 为 5 的题目
  809. const group3 = []; // 其他题目
  810. // 对题目进行分类
  811. questions.forEach(q => {
  812. if (q.category === 10 || q.category === 7) {
  813. group1.push(q);
  814. } else if (q.category === 5) {
  815. group2.push(q);
  816. } else {
  817. group3.push(q);
  818. }
  819. });
  820. // 按照要求的顺序组合题目组
  821. this.questionGroups = [group3, group1, group2].filter(group => group.length > 0);
  822. // 设置当前显示的题目
  823. this.questions = this.questionGroups[0] || [];
  824. },
  825. // 处理继续按钮点击
  826. handleContinue() {
  827. if (this.currentGroupIndex < this.questionGroups.length - 1) {
  828. // 还有下一组题目
  829. this.currentGroupIndex++;
  830. this.questions = this.questionGroups[this.currentGroupIndex];
  831. this.currentQuestionIndex = 0;
  832. this.showContinueButton = false;
  833. this.currentScrollId = 'question-0';
  834. // 重置滚动位置
  835. this.scrollTop = 0;
  836. // 重置相关状态
  837. this.selectedOption = null;
  838. this.selectedOptions = [];
  839. this.showResult = false;
  840. } else {
  841. // 所有组都完成了,跳转到下一个页面
  842. uni.navigateTo({
  843. url: '/pages/interview-question/interview-question'
  844. });
  845. }
  846. },
  847. },
  848. // 添加生命周期钩子,确保在组件销毁时清除计时器
  849. beforeDestroy() {
  850. if (this.timerInterval) {
  851. clearInterval(this.timerInterval);
  852. }
  853. }
  854. }
  855. </script>
  856. <style>
  857. .camera-container {
  858. position: relative;
  859. width: 100%;
  860. height: 100vh;
  861. background-color: #ffffff;
  862. display: flex;
  863. flex-direction: column;
  864. overflow: hidden; /* 防止内容溢出 */
  865. }
  866. .header {
  867. height: 90rpx;
  868. /* background-color: #000000; */
  869. display: flex;
  870. align-items: center;
  871. padding: 0 30rpx;
  872. /* color: #ffffff; */
  873. }
  874. .back-button {
  875. width: 60rpx;
  876. height: 60rpx;
  877. display: flex;
  878. align-items: center;
  879. justify-content: center;
  880. }
  881. .title {
  882. flex: 1;
  883. text-align: center;
  884. font-size: 32rpx;
  885. font-weight: bold;
  886. }
  887. .controls {
  888. display: flex;
  889. gap: 30rpx;
  890. }
  891. .iconfont {
  892. font-size: 40rpx;
  893. }
  894. .content {
  895. flex: 1;
  896. display: flex;
  897. flex-direction: column;
  898. position: relative;
  899. padding: 50rpx 20rpx 0;
  900. }
  901. .progress-container {
  902. display: flex;
  903. flex-direction: column;
  904. justify-content: space-between;
  905. margin-bottom: 20rpx;
  906. }
  907. .progress-step {
  908. display: flex;
  909. align-items: center;
  910. margin-bottom: 10rpx;
  911. }
  912. .step-circle {
  913. width: 40rpx;
  914. height: 40rpx;
  915. border-radius: 50%;
  916. background-color: #e0e0e0;
  917. display: flex;
  918. align-items: center;
  919. justify-content: center;
  920. font-size: 24rpx;
  921. margin-right: 10rpx;
  922. }
  923. .step-circle.active {
  924. background-color: #ff9500;
  925. color: #ffffff;
  926. }
  927. .step-text {
  928. font-size: 24rpx;
  929. color: #666666;
  930. }
  931. .progress-bar {
  932. height: 10rpx;
  933. background-color: #e0e0e0;
  934. border-radius: 5rpx;
  935. overflow: hidden;
  936. margin-bottom: 20rpx;
  937. }
  938. .progress-fill {
  939. height: 100%;
  940. background-color: #00c853;
  941. transition: width 0.3s;
  942. }
  943. .interview-content {
  944. flex: 1;
  945. display: flex;
  946. flex-direction: column;
  947. margin-bottom: 40rpx;
  948. padding: 20rpx;
  949. border-bottom: 1px solid #eee;
  950. }
  951. .video-area {
  952. display: flex;
  953. /* flex-direction: column; */
  954. justify-content: space-between;
  955. height: auto;
  956. margin-bottom: 20rpx;
  957. }
  958. .video-camera {
  959. display: flex;
  960. flex-direction: row;
  961. align-items: center;
  962. justify-content: space-between;
  963. width: 65%;
  964. }
  965. .interviewer {
  966. background-color: #f5f5f5;
  967. display: flex;
  968. align-items: center;
  969. justify-content: center;
  970. position: relative;
  971. width: 48% !important;
  972. height: 300rpx;
  973. }
  974. .interviewer-video {
  975. width: 100%;
  976. height: 100%;
  977. object-fit: cover;
  978. }
  979. .interviewer-avatar {
  980. width: 80%;
  981. height: 80%;
  982. object-fit: contain;
  983. }
  984. .user-camera-container {
  985. width: 48%;
  986. height: 300rpx;
  987. border-radius: 10rpx;
  988. overflow: hidden;
  989. border: 4rpx solid #ffffff;
  990. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
  991. }
  992. .question-area {
  993. flex: 1;
  994. display: flex;
  995. flex-direction: column;
  996. }
  997. .question-number {
  998. font-size: 28rpx;
  999. color: #666;
  1000. margin-bottom: 20rpx;
  1001. }
  1002. .question-importance {
  1003. background-color: #f9f9f9;
  1004. padding: 20rpx;
  1005. border-radius: 10rpx;
  1006. margin-bottom: 20rpx;
  1007. font-size: 28rpx;
  1008. color: #333;
  1009. }
  1010. .question-importance text {
  1011. color: #ff0000;
  1012. /* margin-right: 10rpx; */
  1013. }
  1014. .options {
  1015. display: flex;
  1016. flex-direction: column;
  1017. gap: 20rpx;
  1018. margin-bottom: 30rpx;
  1019. }
  1020. .option-item {
  1021. padding: 20rpx;
  1022. background-color: #f5f5f5;
  1023. border-radius: 10rpx;
  1024. font-size: 28rpx;
  1025. transition: all 0.3s;
  1026. border: 1px solid #e0e0e0;
  1027. display: flex;
  1028. align-items: center;
  1029. cursor: pointer; /* 添加指针样式 */
  1030. }
  1031. .option-item:active {
  1032. transform: scale(0.98); /* 点击时轻微缩小效果 */
  1033. opacity: 0.9;
  1034. }
  1035. .option-selected {
  1036. background-color: #e6f7ff;
  1037. border: 1px solid #1890ff;
  1038. }
  1039. /* 添加选中后的动画效果 */
  1040. @keyframes selectedPulse {
  1041. 0% { box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.4); }
  1042. 70% { box-shadow: 0 0 0 10px rgba(24, 144, 255, 0); }
  1043. 100% { box-shadow: 0 0 0 0 rgba(24, 144, 255, 0); }
  1044. }
  1045. .option-selected {
  1046. animation: selectedPulse 1.5s ease-out;
  1047. }
  1048. .option-correct {
  1049. background-color: #d4f7d4;
  1050. border: 1px solid #52c41a;
  1051. }
  1052. .option-wrong {
  1053. background-color: #fff1f0;
  1054. border: 1px solid #ff4d4f;
  1055. }
  1056. .option-text {
  1057. color: #333;
  1058. }
  1059. .timer-container {
  1060. text-align: right;
  1061. margin: 20rpx 0;
  1062. }
  1063. .timer-text {
  1064. font-size: 24rpx;
  1065. color: #666;
  1066. background-color: #f0f0f0;
  1067. padding: 6rpx 12rpx;
  1068. border-radius: 6rpx;
  1069. }
  1070. .bottom-button-container {
  1071. position: fixed;
  1072. bottom: 50px;
  1073. left: 5%;
  1074. right: 0;
  1075. padding: 20rpx;
  1076. display: flex;
  1077. justify-content: center;
  1078. z-index: 10;
  1079. }
  1080. .bottom-button-wrapper {
  1081. display: flex;
  1082. width: 100%;
  1083. justify-content: space-between;
  1084. align-items: center;
  1085. }
  1086. .timer-display {
  1087. font-size: 28rpx;
  1088. color: #666;
  1089. padding: 10rpx 20rpx;
  1090. border-radius: 6rpx;
  1091. }
  1092. .next-button {
  1093. background-color: #0039b3;
  1094. color: #ffffff;
  1095. border-radius: 10rpx;
  1096. font-size: 30rpx;
  1097. height: 80rpx;
  1098. line-height: 80rpx;
  1099. width: 90%;
  1100. text-align: center;
  1101. margin: 0;
  1102. }
  1103. .footer {
  1104. height: 100rpx;
  1105. background-color: #000000;
  1106. display: flex;
  1107. justify-content: space-between;
  1108. align-items: center;
  1109. padding: 0 30rpx;
  1110. color: #ffffff;
  1111. }
  1112. .timer {
  1113. font-size: 28rpx;
  1114. }
  1115. .next-step {
  1116. font-size: 28rpx;
  1117. }
  1118. .interview-end-modal {
  1119. position: fixed;
  1120. top: 0;
  1121. left: 0;
  1122. right: 0;
  1123. bottom: 0;
  1124. background-color: rgba(0, 0, 0, 0.5);
  1125. display: flex;
  1126. align-items: center;
  1127. justify-content: center;
  1128. z-index: 999;
  1129. }
  1130. .modal-content {
  1131. width: 80%;
  1132. background-color: #ffffff;
  1133. border-radius: 20rpx;
  1134. padding: 40rpx;
  1135. display: flex;
  1136. flex-direction: column;
  1137. align-items: center;
  1138. }
  1139. .modal-title {
  1140. font-size: 36rpx;
  1141. font-weight: bold;
  1142. margin-bottom: 30rpx;
  1143. }
  1144. .score-display {
  1145. font-size: 48rpx;
  1146. font-weight: bold;
  1147. color: #0039b3;
  1148. margin: 20rpx 0;
  1149. }
  1150. .modal-message {
  1151. font-size: 28rpx;
  1152. color: #666;
  1153. text-align: center;
  1154. margin-bottom: 40rpx;
  1155. }
  1156. .modal-buttons {
  1157. display: flex;
  1158. justify-content: space-between;
  1159. width: 100%;
  1160. margin-top: 20rpx;
  1161. }
  1162. .modal-button {
  1163. width: 45%;
  1164. }
  1165. @keyframes speaking {
  1166. 0% {
  1167. opacity: 0.8;
  1168. }
  1169. 50% {
  1170. opacity: 1;
  1171. }
  1172. 100% {
  1173. opacity: 0.8;
  1174. }
  1175. }
  1176. .speaking {
  1177. animation: speaking 1.5s infinite;
  1178. }
  1179. .digital-avatar {
  1180. display: flex;
  1181. flex-direction: row;
  1182. align-items: flex-start;
  1183. justify-content: space-between;
  1184. width: 100%;
  1185. margin-bottom: 20rpx;
  1186. }
  1187. .message-box {
  1188. width: 65%;
  1189. min-height: 120rpx;
  1190. background-color: #f5f5f5;
  1191. border-radius: 10rpx;
  1192. padding: 15rpx;
  1193. margin-right: 20rpx;
  1194. display: flex;
  1195. align-items: center;
  1196. }
  1197. .message-text {
  1198. font-size: 24rpx;
  1199. color: #333;
  1200. line-height: 1.5;
  1201. }
  1202. .user-avatar {
  1203. width: 110px;
  1204. height: 160px;
  1205. border-radius: 10rpx;
  1206. overflow: hidden;
  1207. }
  1208. .camera {
  1209. width: 100%;
  1210. height: 100%;
  1211. border-radius: 10rpx;
  1212. border: 2rpx solid #e0e0e0;
  1213. }
  1214. .avatar-image {
  1215. width: 100%;
  1216. height: 100%;
  1217. border-radius: 10rpx;
  1218. border: 2rpx solid #e0e0e0;
  1219. }
  1220. .digital-human-webview {
  1221. width: 100%;
  1222. height: 100%;
  1223. border-radius: 10rpx;
  1224. border: 2rpx solid #e0e0e0;
  1225. }
  1226. .interview-complete-screen {
  1227. position: fixed;
  1228. top: 0;
  1229. left: 0;
  1230. right: 0;
  1231. bottom: 0;
  1232. background-color: #f0f5ff;
  1233. display: flex;
  1234. flex-direction: column;
  1235. align-items: center;
  1236. justify-content: center;
  1237. z-index: 1000;
  1238. padding: 40rpx;
  1239. }
  1240. .digital-avatar-large {
  1241. width: 200rpx;
  1242. height: 200rpx;
  1243. margin-bottom: 60rpx;
  1244. }
  1245. .avatar-image-large {
  1246. width: 100%;
  1247. height: 100%;
  1248. border-radius: 30rpx;
  1249. }
  1250. .complete-message {
  1251. font-size: 40rpx;
  1252. font-weight: bold;
  1253. color: #333;
  1254. margin-bottom: 30rpx;
  1255. }
  1256. .complete-description {
  1257. font-size: 28rpx;
  1258. color: #666;
  1259. text-align: center;
  1260. margin-bottom: 80rpx;
  1261. line-height: 1.5;
  1262. }
  1263. .complete-button {
  1264. background-color: #0039b3;
  1265. color: #ffffff;
  1266. border-radius: 10rpx;
  1267. font-size: 32rpx;
  1268. padding: 20rpx 60rpx;
  1269. margin-top: 40rpx;
  1270. }
  1271. /* 添加加载状态样式 */
  1272. .loading-container {
  1273. position: absolute;
  1274. top: 0;
  1275. left: 0;
  1276. right: 0;
  1277. bottom: 0;
  1278. display: flex;
  1279. flex-direction: column;
  1280. align-items: center;
  1281. justify-content: center;
  1282. background-color: rgba(255, 255, 255, 0.9);
  1283. z-index: 100;
  1284. }
  1285. .loading-text {
  1286. margin-top: 20rpx;
  1287. font-size: 28rpx;
  1288. color: #666;
  1289. }
  1290. .error-container {
  1291. text-align: center;
  1292. }
  1293. .error-message {
  1294. font-size: 28rpx;
  1295. color: #ff4d4f;
  1296. margin-bottom: 30rpx;
  1297. }
  1298. .retry-button {
  1299. background-color: #0039b3;
  1300. color: #ffffff;
  1301. border-radius: 10rpx;
  1302. font-size: 28rpx;
  1303. padding: 10rpx 30rpx;
  1304. }
  1305. .question-type {
  1306. display: inline-block;
  1307. /* background-color: #0039b3; */
  1308. color: white;
  1309. font-size: 24rpx;
  1310. /* padding: 4rpx 5rpx; */
  1311. border-radius: 6rpx;
  1312. /* margin-right: 10rpx; */
  1313. }
  1314. /* 新增的开放问题样式 */
  1315. .open-question-container {
  1316. display: flex;
  1317. flex-direction: column;
  1318. width: 100%;
  1319. margin-bottom: 20rpx;
  1320. }
  1321. .open-question-input {
  1322. width: 100%;
  1323. height: 300rpx;
  1324. background-color: #f9f9f9;
  1325. border: 1px solid #e0e0e0;
  1326. border-radius: 10rpx;
  1327. padding: 20rpx;
  1328. font-size: 28rpx;
  1329. color: #333;
  1330. }
  1331. .word-count {
  1332. text-align: right;
  1333. font-size: 24rpx;
  1334. color: #999;
  1335. margin-top: 10rpx;
  1336. }
  1337. .option-prefix {
  1338. margin-right: 10rpx;
  1339. font-weight: bold;
  1340. color: #333;
  1341. min-width: 30rpx;
  1342. }
  1343. /* 新增的图片显示样式 */
  1344. .question-image-container {
  1345. width: 100%;
  1346. margin: 20rpx 0;
  1347. display: flex;
  1348. justify-content: center;
  1349. align-items: center;
  1350. }
  1351. .question-image {
  1352. width: 100%;
  1353. max-width: 600rpx;
  1354. border-radius: 10rpx;
  1355. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  1356. }
  1357. /* 添加滚动视图样式 */
  1358. .content-scroll {
  1359. flex: 1;
  1360. height: calc(100vh - 200px); /* 只减去顶部相机的高度 */
  1361. margin-top: 10px;
  1362. }
  1363. /* 添加底部空白区域,防止内容被固定按钮遮挡 */
  1364. .bottom-space {
  1365. height: 50rpx; /* 减小底部空白区域 */
  1366. }
  1367. /* 添加流式布局相关样式 */
  1368. .interview-content {
  1369. margin-bottom: 40rpx;
  1370. padding: 20rpx;
  1371. border-bottom: 1px solid #eee;
  1372. }
  1373. .option-disabled {
  1374. opacity: 0.7;
  1375. background-color: #f0f0f0;
  1376. }
  1377. .question-button-container {
  1378. margin-top: 20rpx;
  1379. display: flex;
  1380. justify-content: center;
  1381. }
  1382. .bottom-timer-container {
  1383. display: none;
  1384. }
  1385. /* 添加固定相机容器样式 */
  1386. .fixed-camera-container {
  1387. position: sticky;
  1388. top: 0;
  1389. width: 100%;
  1390. padding: 20rpx;
  1391. background-color: #fff;
  1392. z-index: 10;
  1393. display: flex;
  1394. justify-content: flex-start;
  1395. border-bottom: 1px solid #eee;
  1396. }
  1397. .user-avatar {
  1398. width: 110px;
  1399. height: 160px;
  1400. border-radius: 10rpx;
  1401. overflow: hidden;
  1402. }
  1403. /* 添加继续按钮样式 */
  1404. .continue-button-container {
  1405. display: flex;
  1406. justify-content: center;
  1407. padding: 20rpx;
  1408. margin-top: 20rpx;
  1409. }
  1410. .continue-button {
  1411. background-color: #0039b3;
  1412. color: #ffffff;
  1413. border-radius: 10rpx;
  1414. font-size: 30rpx;
  1415. height: 80rpx;
  1416. line-height: 80rpx;
  1417. width: 90%;
  1418. text-align: center;
  1419. }
  1420. </style>