camera.vue 43 KB

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