camera.vue 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674
  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. if (this._isClicking) {
  684. return;
  685. }
  686. this._isClicking = true;
  687. // 清除之前的计时器
  688. if (this._clickTimer) {
  689. clearTimeout(this._clickTimer);
  690. }
  691. try {
  692. // 如果是当前题目,调用选择逻辑
  693. this.selectOption(index);
  694. // 设置延时,防止快速重复点击
  695. this._clickTimer = setTimeout(() => {
  696. this._isClicking = false;
  697. }, 1000);
  698. } catch (error) {
  699. console.error('处理选项点击失败:', error);
  700. uni.showToast({
  701. title: '操作失败,请重试',
  702. icon: 'none'
  703. });
  704. this._isClicking = false;
  705. }
  706. },
  707. // 处理题目分组
  708. processQuestionGroups(questions) {
  709. // 创建两个分组
  710. const group1 = []; // category 为 5 的题目
  711. const group2 = []; // 其他题目
  712. // 对题目进行分类
  713. questions.forEach(q => {
  714. if (q.category === 5) {
  715. group1.push(q);
  716. } else {
  717. group2.push(q);
  718. }
  719. });
  720. // 按照要求的顺序组合题目组:先显示其他题目,再显示 category 为 5 的题目
  721. this.questionGroups = [group2, group1].filter(group => group.length > 0);
  722. // 设置当前显示的题目
  723. this.questions = this.questionGroups[0] || [];
  724. },
  725. // 处理继续按钮点击
  726. handleContinue() {
  727. if (this.currentGroupIndex < this.questionGroups.length - 1) {
  728. // 还有下一组题目
  729. this.currentGroupIndex++;
  730. this.questions = this.questionGroups[this.currentGroupIndex];
  731. this.currentQuestionIndex = 0;
  732. this.showContinueButton = false;
  733. this.currentScrollId = 'question-0';
  734. // 重置滚动位置
  735. this.scrollTop = 0;
  736. // 重置相关状态
  737. this.selectedOption = null;
  738. this.selectedOptions = [];
  739. this.showResult = false;
  740. } else {
  741. // 所有组都完成了,跳转到下一个页面
  742. uni.navigateTo({
  743. url: '/pages/interview-question/interview-question'
  744. });
  745. }
  746. },
  747. // 保存当前题目的答案
  748. saveAnswer() {
  749. let answer;
  750. if (this.currentQuestion.questionType === 0) { // 开放问题
  751. answer = {
  752. questionId: this.currentQuestion.id,
  753. questionType: this.currentQuestion.questionType,
  754. answer: this.openQuestionAnswer,
  755. answerDuration: this.getAnswerDuration() // 添加答题时长
  756. };
  757. } else if (this.currentQuestion.questionType === 1 || this.currentQuestion.questionType === 3 || this.currentQuestion.questionType === 4) { // 单选题、看图答题或类型4题目
  758. answer = {
  759. questionId: this.currentQuestion.id,
  760. questionType: this.currentQuestion.questionType,
  761. answer: this.selectedOption,
  762. answerDuration: this.getAnswerDuration() // 添加答题时长
  763. };
  764. } else { // 多选题
  765. answer = {
  766. questionId: this.currentQuestion.id,
  767. questionType: this.currentQuestion.questionType,
  768. answer: this.selectedOptions,
  769. answerDuration: this.getAnswerDuration() // 添加答题时长
  770. };
  771. }
  772. // 检查是否已存在该题目的答案,如果存在则更新
  773. const existingIndex = this.answers.findIndex(a => a.questionId === answer.questionId);
  774. if (existingIndex > -1) {
  775. this.answers[existingIndex] = answer;
  776. } else {
  777. this.answers.push(answer);
  778. }
  779. // 保存当前答案以便提交
  780. this.currentAnswer = answer;
  781. console.log('已保存答案:', this.answers);
  782. },
  783. // 获取答题时长(秒)
  784. getAnswerDuration() {
  785. // 这里假设每题默认30秒,根据剩余时间计算用时
  786. // 您可以根据实际情况调整计算逻辑
  787. const remainingTimeArr = this.remainingTime.split(':');
  788. const remainingSeconds = parseInt(remainingTimeArr[0]) * 60 + parseInt(remainingTimeArr[1]);
  789. return 30 - remainingSeconds; // 假设每题30秒
  790. },
  791. // 提交当前答案
  792. async submitCurrentAnswer() {
  793. if (!this.currentAnswer || this.isSubmitting) return; // 如果正在提交或没有答案,则不执行
  794. try {
  795. // 设置提交状态为true
  796. this.isSubmitting = true;
  797. // 显示提交中提示
  798. uni.showLoading({
  799. title: '正在提交答案...'
  800. });
  801. // 构建提交数据
  802. let answerContent = '';
  803. let answerOptions = [];
  804. if (this.currentAnswer.questionType === 0) {
  805. // 开放题直接使用文本答案
  806. answerContent = this.currentAnswer.answer;
  807. answerOptions = [];
  808. } else if (this.currentAnswer.questionType === 1 || this.currentAnswer.questionType === 3 || this.currentAnswer.questionType === 4) {
  809. // 单选题或看图答题,获取选项ID而不是索引
  810. const selectedIndex = this.currentAnswer.answer;
  811. const selectedOption = this.currentQuestion.options[selectedIndex];
  812. console.log('selectedOption',selectedOption);
  813. const optionId = selectedOption.id ? selectedOption.id : selectedIndex;
  814. answerContent = optionId.toString();
  815. answerOptions = [optionId];
  816. } else if (this.currentAnswer.questionType === 2) {
  817. // 多选题,将选项ID数组转为逗号分隔的字符串
  818. const selectedIndices = this.currentAnswer.answer;
  819. const selectedOptionIds = selectedIndices.map(index => {
  820. const option = this.currentQuestion.options[index];
  821. // 使用选项的ID或其他唯一标识符
  822. return option.id ? option.id : index;
  823. });
  824. answerOptions = selectedOptionIds;
  825. }
  826. const submitData = {
  827. position_id: JSON.parse(uni.getStorageSync('selectedJob')).id,
  828. applicant_id: JSON.parse(uni.getStorageSync('userInfo')).id,
  829. job_position_question_id: this.currentAnswer.questionId,
  830. // answer_content: answerContent,
  831. answer_options: answerOptions,
  832. answer_duration: this.currentAnswer.answerDuration || 0,
  833. tenant_id:JSON.parse(uni.getStorageSync('userInfo')).tenant_id || 1
  834. };
  835. console.log('提交数据:', submitData);
  836. // 调用API提交答案
  837. const res = await this.$http.post(`${apiBaseUrl}/api/job/submit_answer`, submitData);
  838. console.log('提交答案响应:', res);
  839. return res;
  840. } catch (error) {
  841. console.error('提交答案失败:', error);
  842. // 显示错误提示
  843. uni.showToast({
  844. title: '提交答案失败,请重试',
  845. icon: 'none'
  846. });
  847. throw error;
  848. } finally {
  849. // 隐藏加载提示
  850. uni.hideLoading();
  851. // 重置提交状态
  852. this.isSubmitting = false;
  853. }
  854. },
  855. // 修改 goToNextQuestion 方法
  856. async goToNextQuestion() {
  857. // 检查是否完成当前组的所有题目
  858. if (this.currentQuestionIndex >= this.questions.length - 1) {
  859. // 当前组的所有题目都已完成,显示继续按钮
  860. this.showContinueButton = true;
  861. // 使用延时确保按钮渲染完成后再滚动
  862. setTimeout(() => {
  863. // 先重置 scrollTop,然后再设置新值,强制触发更新
  864. this.scrollTop = 0;
  865. // 使用 nextTick 确保视图已更新
  866. this.$nextTick(() => {
  867. // 延迟设置新的滚动位置
  868. setTimeout(() => {
  869. this.scrollTop = 10000; // 一个足够大的值
  870. }, 100);
  871. });
  872. }, 200);
  873. return;
  874. }
  875. // 如果还有下一题,继续下一题
  876. this.currentQuestionIndex++;
  877. this.currentScrollId = 'question-' + this.currentQuestionIndex;
  878. // 重置状态(只在非最后一题时重置)
  879. this.showResult = false;
  880. this.selectedOption = null;
  881. this.selectedOptions = [];
  882. this.openQuestionAnswer = '';
  883. // 如果有下一题的详情
  884. if (this.questions[this.currentQuestionIndex]) {
  885. // 更新进度
  886. this.progressWidth = (this.currentQuestionIndex + 1) / this.questions.length * 100;
  887. // 重置计时器
  888. this.resetTimer();
  889. // 播放AI介绍下一题
  890. this.playAiSpeaking();
  891. setTimeout(() => {
  892. this.pauseAiSpeaking();
  893. }, 2000);
  894. }
  895. },
  896. },
  897. // 添加生命周期钩子,确保在组件销毁时清除计时器
  898. beforeDestroy() {
  899. if (this.timerInterval) {
  900. clearInterval(this.timerInterval);
  901. }
  902. }
  903. }
  904. </script>
  905. <style>
  906. .camera-container {
  907. position: relative;
  908. width: 100%;
  909. height: 100vh;
  910. background-color: #ffffff;
  911. display: flex;
  912. flex-direction: column;
  913. overflow: hidden; /* 防止内容溢出 */
  914. }
  915. .header {
  916. height: 90rpx;
  917. /* background-color: #000000; */
  918. display: flex;
  919. align-items: center;
  920. padding: 0 30rpx;
  921. /* color: #ffffff; */
  922. }
  923. .back-button {
  924. width: 60rpx;
  925. height: 60rpx;
  926. display: flex;
  927. align-items: center;
  928. justify-content: center;
  929. }
  930. .title {
  931. flex: 1;
  932. text-align: center;
  933. font-size: 32rpx;
  934. font-weight: bold;
  935. }
  936. .controls {
  937. display: flex;
  938. gap: 30rpx;
  939. }
  940. .iconfont {
  941. font-size: 40rpx;
  942. }
  943. .content {
  944. flex: 1;
  945. display: flex;
  946. flex-direction: column;
  947. position: relative;
  948. padding: 50rpx 20rpx 0;
  949. }
  950. .progress-container {
  951. display: flex;
  952. flex-direction: column;
  953. justify-content: space-between;
  954. margin-bottom: 20rpx;
  955. }
  956. .progress-step {
  957. display: flex;
  958. align-items: center;
  959. margin-bottom: 10rpx;
  960. }
  961. .step-circle {
  962. width: 40rpx;
  963. height: 40rpx;
  964. border-radius: 50%;
  965. background-color: #e0e0e0;
  966. display: flex;
  967. align-items: center;
  968. justify-content: center;
  969. font-size: 24rpx;
  970. margin-right: 10rpx;
  971. }
  972. .step-circle.active {
  973. background-color: #ff9500;
  974. color: #ffffff;
  975. }
  976. .step-text {
  977. font-size: 24rpx;
  978. color: #666666;
  979. }
  980. .progress-bar {
  981. height: 10rpx;
  982. background-color: #e0e0e0;
  983. border-radius: 5rpx;
  984. overflow: hidden;
  985. margin-bottom: 20rpx;
  986. }
  987. .progress-fill {
  988. height: 100%;
  989. background-color: #00c853;
  990. transition: width 0.3s;
  991. }
  992. .interview-content {
  993. flex: 1;
  994. display: flex;
  995. flex-direction: column;
  996. margin-bottom: 40rpx;
  997. padding: 20rpx;
  998. border-bottom: 1px solid #eee;
  999. }
  1000. .video-area {
  1001. display: flex;
  1002. /* flex-direction: column; */
  1003. justify-content: space-between;
  1004. height: auto;
  1005. margin-bottom: 20rpx;
  1006. }
  1007. .video-camera {
  1008. display: flex;
  1009. flex-direction: row;
  1010. align-items: center;
  1011. justify-content: space-between;
  1012. width: 65%;
  1013. }
  1014. .interviewer {
  1015. background-color: #f5f5f5;
  1016. display: flex;
  1017. align-items: center;
  1018. justify-content: center;
  1019. position: relative;
  1020. width: 48% !important;
  1021. height: 300rpx;
  1022. }
  1023. .interviewer-video {
  1024. width: 100%;
  1025. height: 100%;
  1026. object-fit: cover;
  1027. }
  1028. .interviewer-avatar {
  1029. width: 80%;
  1030. height: 80%;
  1031. object-fit: contain;
  1032. }
  1033. .user-camera-container {
  1034. width: 48%;
  1035. height: 300rpx;
  1036. border-radius: 10rpx;
  1037. overflow: hidden;
  1038. border: 4rpx solid #ffffff;
  1039. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
  1040. }
  1041. .question-area {
  1042. flex: 1;
  1043. display: flex;
  1044. flex-direction: column;
  1045. }
  1046. .question-number {
  1047. font-size: 28rpx;
  1048. color: #666;
  1049. margin-bottom: 20rpx;
  1050. }
  1051. .question-importance {
  1052. background-color: #f9f9f9;
  1053. padding: 20rpx;
  1054. border-radius: 10rpx;
  1055. margin-bottom: 20rpx;
  1056. font-size: 28rpx;
  1057. color: #333;
  1058. }
  1059. .question-importance text {
  1060. color: #ff0000;
  1061. /* margin-right: 10rpx; */
  1062. }
  1063. .options {
  1064. display: flex;
  1065. flex-direction: column;
  1066. gap: 20rpx;
  1067. margin-bottom: 30rpx;
  1068. }
  1069. .option-item {
  1070. padding: 20rpx;
  1071. background-color: #f5f5f5;
  1072. border-radius: 10rpx;
  1073. font-size: 28rpx;
  1074. transition: all 0.3s;
  1075. border: 1px solid #e0e0e0;
  1076. display: flex;
  1077. align-items: center;
  1078. cursor: pointer; /* 添加指针样式 */
  1079. }
  1080. .option-item:active {
  1081. transform: scale(0.98); /* 点击时轻微缩小效果 */
  1082. opacity: 0.9;
  1083. }
  1084. .option-selected {
  1085. background-color: #e6f7ff;
  1086. border: 1px solid #1890ff;
  1087. }
  1088. /* 添加选中后的动画效果 */
  1089. @keyframes selectedPulse {
  1090. 0% { box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.4); }
  1091. 70% { box-shadow: 0 0 0 10px rgba(24, 144, 255, 0); }
  1092. 100% { box-shadow: 0 0 0 0 rgba(24, 144, 255, 0); }
  1093. }
  1094. .option-selected {
  1095. animation: selectedPulse 1.5s ease-out;
  1096. }
  1097. .option-correct {
  1098. background-color: #d4f7d4;
  1099. border: 1px solid #52c41a;
  1100. }
  1101. .option-wrong {
  1102. background-color: #fff1f0;
  1103. border: 1px solid #ff4d4f;
  1104. }
  1105. .option-text {
  1106. color: #333;
  1107. }
  1108. .timer-container {
  1109. text-align: right;
  1110. margin: 20rpx 0;
  1111. }
  1112. .timer-text {
  1113. font-size: 24rpx;
  1114. color: #666;
  1115. background-color: #f0f0f0;
  1116. padding: 6rpx 12rpx;
  1117. border-radius: 6rpx;
  1118. }
  1119. .bottom-button-container {
  1120. position: fixed;
  1121. bottom: 50px;
  1122. left: 5%;
  1123. right: 0;
  1124. padding: 20rpx;
  1125. display: flex;
  1126. justify-content: center;
  1127. z-index: 10;
  1128. }
  1129. .bottom-button-wrapper {
  1130. display: flex;
  1131. width: 100%;
  1132. justify-content: space-between;
  1133. align-items: center;
  1134. }
  1135. .timer-display {
  1136. font-size: 28rpx;
  1137. color: #666;
  1138. padding: 10rpx 20rpx;
  1139. border-radius: 6rpx;
  1140. }
  1141. .next-button {
  1142. background-color: #0039b3;
  1143. color: #ffffff;
  1144. border-radius: 10rpx;
  1145. font-size: 30rpx;
  1146. height: 80rpx;
  1147. line-height: 80rpx;
  1148. width: 90%;
  1149. text-align: center;
  1150. margin: 0;
  1151. }
  1152. .footer {
  1153. height: 100rpx;
  1154. background-color: #000000;
  1155. display: flex;
  1156. justify-content: space-between;
  1157. align-items: center;
  1158. padding: 0 30rpx;
  1159. color: #ffffff;
  1160. }
  1161. .timer {
  1162. font-size: 28rpx;
  1163. }
  1164. .next-step {
  1165. font-size: 28rpx;
  1166. }
  1167. .interview-end-modal {
  1168. position: fixed;
  1169. top: 0;
  1170. left: 0;
  1171. right: 0;
  1172. bottom: 0;
  1173. background-color: rgba(0, 0, 0, 0.5);
  1174. display: flex;
  1175. align-items: center;
  1176. justify-content: center;
  1177. z-index: 999;
  1178. }
  1179. .modal-content {
  1180. width: 80%;
  1181. background-color: #ffffff;
  1182. border-radius: 20rpx;
  1183. padding: 40rpx;
  1184. display: flex;
  1185. flex-direction: column;
  1186. align-items: center;
  1187. }
  1188. .modal-title {
  1189. font-size: 36rpx;
  1190. font-weight: bold;
  1191. margin-bottom: 30rpx;
  1192. }
  1193. .score-display {
  1194. font-size: 48rpx;
  1195. font-weight: bold;
  1196. color: #0039b3;
  1197. margin: 20rpx 0;
  1198. }
  1199. .modal-message {
  1200. font-size: 28rpx;
  1201. color: #666;
  1202. text-align: center;
  1203. margin-bottom: 40rpx;
  1204. }
  1205. .modal-buttons {
  1206. display: flex;
  1207. justify-content: space-between;
  1208. width: 100%;
  1209. margin-top: 20rpx;
  1210. }
  1211. .modal-button {
  1212. width: 45%;
  1213. }
  1214. @keyframes speaking {
  1215. 0% {
  1216. opacity: 0.8;
  1217. }
  1218. 50% {
  1219. opacity: 1;
  1220. }
  1221. 100% {
  1222. opacity: 0.8;
  1223. }
  1224. }
  1225. .speaking {
  1226. animation: speaking 1.5s infinite;
  1227. }
  1228. .digital-avatar {
  1229. display: flex;
  1230. flex-direction: row;
  1231. align-items: flex-start;
  1232. justify-content: space-between;
  1233. width: 100%;
  1234. margin-bottom: 20rpx;
  1235. }
  1236. .message-box {
  1237. width: 65%;
  1238. min-height: 120rpx;
  1239. background-color: #f5f5f5;
  1240. border-radius: 10rpx;
  1241. padding: 15rpx;
  1242. margin-right: 20rpx;
  1243. display: flex;
  1244. align-items: center;
  1245. }
  1246. .message-text {
  1247. font-size: 24rpx;
  1248. color: #333;
  1249. line-height: 1.5;
  1250. }
  1251. .user-avatar {
  1252. width: 110px;
  1253. height: 160px;
  1254. border-radius: 10rpx;
  1255. overflow: hidden;
  1256. }
  1257. .camera {
  1258. width: 100%;
  1259. height: 100%;
  1260. border-radius: 10rpx;
  1261. border: 2rpx solid #e0e0e0;
  1262. }
  1263. .avatar-image {
  1264. width: 100%;
  1265. height: 100%;
  1266. border-radius: 10rpx;
  1267. border: 2rpx solid #e0e0e0;
  1268. }
  1269. .digital-human-webview {
  1270. width: 100%;
  1271. height: 100%;
  1272. border-radius: 10rpx;
  1273. border: 2rpx solid #e0e0e0;
  1274. }
  1275. .interview-complete-screen {
  1276. position: fixed;
  1277. top: 0;
  1278. left: 0;
  1279. right: 0;
  1280. bottom: 0;
  1281. background-color: #f0f5ff;
  1282. display: flex;
  1283. flex-direction: column;
  1284. align-items: center;
  1285. justify-content: center;
  1286. z-index: 1000;
  1287. padding: 40rpx;
  1288. }
  1289. .digital-avatar-large {
  1290. width: 200rpx;
  1291. height: 200rpx;
  1292. margin-bottom: 60rpx;
  1293. }
  1294. .avatar-image-large {
  1295. width: 100%;
  1296. height: 100%;
  1297. border-radius: 30rpx;
  1298. }
  1299. .complete-message {
  1300. font-size: 40rpx;
  1301. font-weight: bold;
  1302. color: #333;
  1303. margin-bottom: 30rpx;
  1304. }
  1305. .complete-description {
  1306. font-size: 28rpx;
  1307. color: #666;
  1308. text-align: center;
  1309. margin-bottom: 80rpx;
  1310. line-height: 1.5;
  1311. }
  1312. .complete-button {
  1313. background-color: #0039b3;
  1314. color: #ffffff;
  1315. border-radius: 10rpx;
  1316. font-size: 32rpx;
  1317. padding: 20rpx 60rpx;
  1318. margin-top: 40rpx;
  1319. }
  1320. /* 添加加载状态样式 */
  1321. .loading-container {
  1322. position: absolute;
  1323. top: 0;
  1324. left: 0;
  1325. right: 0;
  1326. bottom: 0;
  1327. display: flex;
  1328. flex-direction: column;
  1329. align-items: center;
  1330. justify-content: center;
  1331. background-color: rgba(255, 255, 255, 0.9);
  1332. z-index: 100;
  1333. }
  1334. .loading-text {
  1335. margin-top: 20rpx;
  1336. font-size: 28rpx;
  1337. color: #666;
  1338. }
  1339. .error-container {
  1340. text-align: center;
  1341. }
  1342. .error-message {
  1343. font-size: 28rpx;
  1344. color: #ff4d4f;
  1345. margin-bottom: 30rpx;
  1346. }
  1347. .retry-button {
  1348. background-color: #0039b3;
  1349. color: #ffffff;
  1350. border-radius: 10rpx;
  1351. font-size: 28rpx;
  1352. padding: 10rpx 30rpx;
  1353. }
  1354. .question-type {
  1355. display: inline-block;
  1356. /* background-color: #0039b3; */
  1357. color: white;
  1358. font-size: 24rpx;
  1359. /* padding: 4rpx 5rpx; */
  1360. border-radius: 6rpx;
  1361. /* margin-right: 10rpx; */
  1362. }
  1363. /* 新增的开放问题样式 */
  1364. .open-question-container {
  1365. display: flex;
  1366. flex-direction: column;
  1367. width: 100%;
  1368. margin-bottom: 20rpx;
  1369. }
  1370. .open-question-input {
  1371. width: 100%;
  1372. height: 300rpx;
  1373. background-color: #f9f9f9;
  1374. border: 1px solid #e0e0e0;
  1375. border-radius: 10rpx;
  1376. padding: 20rpx;
  1377. font-size: 28rpx;
  1378. color: #333;
  1379. }
  1380. .word-count {
  1381. text-align: right;
  1382. font-size: 24rpx;
  1383. color: #999;
  1384. margin-top: 10rpx;
  1385. }
  1386. .option-prefix {
  1387. margin-right: 10rpx;
  1388. font-weight: bold;
  1389. color: #333;
  1390. min-width: 30rpx;
  1391. }
  1392. /* 新增的图片显示样式 */
  1393. .question-image-container {
  1394. width: 100%;
  1395. margin: 20rpx 0;
  1396. display: flex;
  1397. justify-content: center;
  1398. align-items: center;
  1399. }
  1400. .question-image {
  1401. width: 100%;
  1402. max-width: 600rpx;
  1403. border-radius: 10rpx;
  1404. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  1405. }
  1406. /* 添加滚动视图样式 */
  1407. .content-scroll {
  1408. flex: 1;
  1409. height: calc(100vh - 200px); /* 只减去顶部相机的高度 */
  1410. margin-top: 10px;
  1411. }
  1412. /* 添加底部空白区域,防止内容被固定按钮遮挡 */
  1413. .bottom-space {
  1414. height: 50rpx; /* 减小底部空白区域 */
  1415. }
  1416. /* 添加流式布局相关样式 */
  1417. .interview-content {
  1418. margin-bottom: 40rpx;
  1419. padding: 20rpx;
  1420. border-bottom: 1px solid #eee;
  1421. }
  1422. .option-disabled {
  1423. opacity: 0.7;
  1424. background-color: #f0f0f0;
  1425. }
  1426. .question-button-container {
  1427. margin-top: 20rpx;
  1428. display: flex;
  1429. justify-content: center;
  1430. }
  1431. .bottom-timer-container {
  1432. display: none;
  1433. }
  1434. /* 添加固定相机容器样式 */
  1435. .fixed-camera-container {
  1436. position: sticky;
  1437. top: 0;
  1438. width: 100%;
  1439. padding: 20rpx;
  1440. background-color: #fff;
  1441. z-index: 10;
  1442. display: flex;
  1443. justify-content: flex-start;
  1444. border-bottom: 1px solid #eee;
  1445. }
  1446. .user-avatar {
  1447. width: 110px;
  1448. height: 160px;
  1449. border-radius: 10rpx;
  1450. overflow: hidden;
  1451. }
  1452. /* 添加继续按钮样式 */
  1453. .continue-button-container {
  1454. display: flex;
  1455. justify-content: center;
  1456. padding: 20rpx;
  1457. margin-top: 20rpx;
  1458. }
  1459. .continue-button {
  1460. background-color: #0039b3;
  1461. color: #ffffff;
  1462. border-radius: 10rpx;
  1463. font-size: 30rpx;
  1464. height: 80rpx;
  1465. line-height: 80rpx;
  1466. width: 90%;
  1467. text-align: center;
  1468. }
  1469. </style>