camera.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. "use strict";
  2. const common_vendor = require("../../common/vendor.js");
  3. const api_user = require("../../api/user.js");
  4. const common_config = require("../../common/config.js");
  5. const common_assets = require("../../common/assets.js");
  6. const _sfc_main = {
  7. data() {
  8. return {
  9. cameraContext: null,
  10. devicePosition: "front",
  11. interviewStarted: true,
  12. showEndModal: false,
  13. currentQuestionIndex: 0,
  14. currentStep: 1,
  15. stepTexts: ["测试准备", "回答问题", "测试结束"],
  16. progressWidth: 50,
  17. remainingTime: "00:27",
  18. selectedOption: null,
  19. selectedOptions: [],
  20. showResult: false,
  21. isAnswerCorrect: false,
  22. questions: [],
  23. // 改为空数组,将通过API获取
  24. interviewId: null,
  25. // 存储当前面试ID
  26. useVideo: false,
  27. timerInterval: null,
  28. score: 0,
  29. totalQuestions: 0,
  30. interviewCompleted: false,
  31. digitalHumanUrl: "",
  32. // 数字人URL
  33. loading: true,
  34. // 添加加载状态
  35. loadError: false,
  36. // 添加加载错误状态
  37. errorMessage: "",
  38. // 添加错误消息
  39. answers: [],
  40. // 存储用户的所有答案
  41. currentQuestionDetail: null,
  42. // 当前题目详情
  43. isSubmitting: false,
  44. // 是否正在提交答案
  45. openQuestionAnswer: "",
  46. // 存储开放问题的答案
  47. currentAnswer: null,
  48. // 存储当前答案以便提交
  49. questionStartTime: null,
  50. // 存储问题开始时间
  51. mode: "normal",
  52. // 相机模式
  53. currentScrollId: "question-0"
  54. // 当前滚动到的问题ID
  55. };
  56. },
  57. computed: {
  58. currentQuestion() {
  59. console.log(this.questions[this.currentQuestionIndex]);
  60. return this.questions[this.currentQuestionIndex];
  61. }
  62. },
  63. onLoad(options) {
  64. if (options && options.id) {
  65. this.interviewId = options.id;
  66. this.fetchInterviewData();
  67. } else {
  68. this.fetchInterviewList();
  69. }
  70. },
  71. onReady() {
  72. this.cameraContext = common_vendor.index.createCameraContext();
  73. if (this.useVideo) {
  74. this.aiVideoContext = common_vendor.index.createVideoContext("aiInterviewer");
  75. }
  76. this.initDigitalHuman();
  77. },
  78. methods: {
  79. // 获取面试列表
  80. async fetchInterviewList() {
  81. try {
  82. this.loading = true;
  83. const res2 = await api_user.getInterviewList({ job_id: JSON.parse(common_vendor.index.getStorageSync("selectedJob")).id, page: 1, limit: 999 });
  84. console.log(res2);
  85. this.interviewId = res2;
  86. this.fetchInterviewData(res2);
  87. } catch (error) {
  88. console.error("获取面试列表失败:", error);
  89. this.handleLoadError("获取面试列表失败");
  90. }
  91. },
  92. // 获取面试详情数据
  93. async fetchInterviewData(data) {
  94. try {
  95. this.loading = true;
  96. if (data && Array.isArray(data)) {
  97. this.questions = data.filter((q) => q.question_form !== 0).map((q, index) => ({
  98. id: q.id || index + 1,
  99. text: q.question || "未知问题",
  100. options: q.options || [],
  101. correctAnswer: q.correctAnswer || 0,
  102. isImportant: q.is_system || false,
  103. explanation: q.explanation || "",
  104. questionType: q.question_form,
  105. questionTypeName: q.question_form_name || "单选题",
  106. correctAnswers: q.correct_answers || [],
  107. difficulty: q.difficulty || 1,
  108. difficultyName: q.difficulty_name || "初级",
  109. imageUrl: q.question_image_url || ""
  110. // 添加图片URL字段
  111. }));
  112. } else {
  113. this.processInterviewData(res);
  114. }
  115. console.log(this.questions);
  116. this.totalQuestions = this.questions.length;
  117. } catch (error) {
  118. console.error("获取面试详情失败:", error);
  119. this.handleLoadError("获取面试详情失败");
  120. } finally {
  121. this.loading = false;
  122. }
  123. },
  124. // 处理面试数据
  125. processInterviewData(data) {
  126. this.questions = [];
  127. if (data && data.question_form !== 0) {
  128. const formattedQuestion = {
  129. id: data.id || 1,
  130. text: data.question || "未知问题",
  131. options: data.options || [],
  132. correctAnswer: data.correctAnswer || 0,
  133. isImportant: data.is_system || false,
  134. explanation: data.explanation || "",
  135. questionType: data.question_form,
  136. // 1-单选题,2-多选题,3-看图答题
  137. questionTypeName: data.question_form_name || "单选题",
  138. correctAnswers: data.correct_answers || [],
  139. difficulty: data.difficulty || 1,
  140. difficultyName: data.difficulty_name || "初级",
  141. imageUrl: data.question_image_url || ""
  142. // 添加图片URL字段
  143. };
  144. this.questions.push(formattedQuestion);
  145. this.totalQuestions = this.questions.length;
  146. } else {
  147. this.handleLoadError("没有可用的选择题");
  148. }
  149. },
  150. // 处理加载错误
  151. handleLoadError(message) {
  152. this.loadError = true;
  153. this.loading = false;
  154. this.errorMessage = message || "加载失败";
  155. common_vendor.index.showToast({
  156. title: message || "加载失败",
  157. icon: "none",
  158. duration: 2e3
  159. });
  160. },
  161. startTimer() {
  162. if (this.questions.length === 0)
  163. return;
  164. this.questionStartTime = /* @__PURE__ */ new Date();
  165. let seconds = 60;
  166. this.remainingTime = `01:00`;
  167. this.timerInterval = setInterval(() => {
  168. seconds--;
  169. if (seconds <= 0) {
  170. clearInterval(this.timerInterval);
  171. if (!this.showResult) {
  172. this.checkAnswer();
  173. setTimeout(() => {
  174. if (this.currentQuestionIndex < this.questions.length - 1) {
  175. this.goToNextQuestion();
  176. } else {
  177. common_vendor.index.navigateTo({
  178. url: "/pages/interview-question/interview-question"
  179. });
  180. }
  181. }, 1500);
  182. }
  183. }
  184. const min = Math.floor(seconds / 60).toString().padStart(2, "0");
  185. const sec = (seconds % 60).toString().padStart(2, "0");
  186. this.remainingTime = `${min}:${sec}`;
  187. }, 1e3);
  188. },
  189. resetTimer() {
  190. clearInterval(this.timerInterval);
  191. },
  192. selectOption(index) {
  193. if (this.showResult)
  194. return;
  195. if (this.currentQuestion.questionType === 2) {
  196. const optionIndex = this.selectedOptions.indexOf(index);
  197. if (optionIndex > -1) {
  198. this.selectedOptions.splice(optionIndex, 1);
  199. } else {
  200. this.selectedOptions.push(index);
  201. }
  202. } else if (this.currentQuestion.questionType === 1 || this.currentQuestion.questionType === 3) {
  203. this.selectedOption = index;
  204. this.playAiSpeaking();
  205. setTimeout(() => {
  206. this.checkAnswer();
  207. this.saveAnswer();
  208. this.submitCurrentAnswer().then(() => {
  209. setTimeout(() => {
  210. if (this.currentQuestionIndex >= this.questions.length - 1) {
  211. common_vendor.index.navigateTo({
  212. url: "/pages/interview-question/interview-question"
  213. });
  214. } else {
  215. this.goToNextQuestion();
  216. }
  217. }, 800);
  218. }).catch((error) => {
  219. console.error("提交答案失败:", error);
  220. setTimeout(() => {
  221. if (this.currentQuestionIndex >= this.questions.length - 1) {
  222. common_vendor.index.navigateTo({
  223. url: "/pages/interview-question/interview-question"
  224. });
  225. } else {
  226. this.goToNextQuestion();
  227. }
  228. }, 1e3);
  229. });
  230. }, 500);
  231. }
  232. },
  233. checkAnswer() {
  234. if (this.showResult)
  235. return;
  236. clearInterval(this.timerInterval);
  237. if (!this.currentQuestion) {
  238. console.error("当前问题不存在");
  239. return;
  240. }
  241. if (this.currentQuestion.questionType === 0) {
  242. this.isAnswerCorrect = true;
  243. } else if (this.currentQuestion.questionType === 2) {
  244. const sortedSelected = [...this.selectedOptions].sort();
  245. const sortedCorrect = [...this.currentQuestion.correctAnswers].sort();
  246. if (sortedSelected.length !== sortedCorrect.length) {
  247. this.isAnswerCorrect = false;
  248. } else {
  249. this.isAnswerCorrect = sortedSelected.every((value, index) => value === sortedCorrect[index]);
  250. }
  251. } else {
  252. this.isAnswerCorrect = this.selectedOption === this.currentQuestion.correctAnswer;
  253. }
  254. this.showResult = true;
  255. this.saveAnswer();
  256. },
  257. // 修改 nextQuestion 方法,只处理多选题
  258. nextQuestion() {
  259. if (this.currentQuestion.questionType !== 2)
  260. return;
  261. this.checkAnswer();
  262. this.saveAnswer();
  263. this.submitCurrentAnswer().then(() => {
  264. setTimeout(() => {
  265. if (this.currentQuestionIndex >= this.questions.length - 1) {
  266. common_vendor.index.navigateTo({
  267. url: "/pages/interview-question/interview-question"
  268. });
  269. } else {
  270. this.goToNextQuestion();
  271. }
  272. }, 500);
  273. }).catch((error) => {
  274. console.error("提交答案失败:", error);
  275. setTimeout(() => {
  276. if (this.currentQuestionIndex >= this.questions.length - 1) {
  277. common_vendor.index.navigateTo({
  278. url: "/pages/interview-question/interview-question"
  279. });
  280. } else {
  281. this.goToNextQuestion();
  282. }
  283. }, 1e3);
  284. });
  285. },
  286. // 保存当前题目的答案
  287. saveAnswer() {
  288. let answer;
  289. if (this.currentQuestion.questionType === 0) {
  290. answer = {
  291. questionId: this.currentQuestion.id,
  292. questionType: this.currentQuestion.questionType,
  293. answer: this.openQuestionAnswer,
  294. answerDuration: this.getAnswerDuration()
  295. // 添加答题时长
  296. };
  297. } else if (this.currentQuestion.questionType === 1 || this.currentQuestion.questionType === 3) {
  298. answer = {
  299. questionId: this.currentQuestion.id,
  300. questionType: this.currentQuestion.questionType,
  301. answer: this.selectedOption,
  302. answerDuration: this.getAnswerDuration()
  303. // 添加答题时长
  304. };
  305. } else {
  306. answer = {
  307. questionId: this.currentQuestion.id,
  308. questionType: this.currentQuestion.questionType,
  309. answer: this.selectedOptions,
  310. answerDuration: this.getAnswerDuration()
  311. // 添加答题时长
  312. };
  313. }
  314. const existingIndex = this.answers.findIndex((a) => a.questionId === answer.questionId);
  315. if (existingIndex > -1) {
  316. this.answers[existingIndex] = answer;
  317. } else {
  318. this.answers.push(answer);
  319. }
  320. this.currentAnswer = answer;
  321. console.log("已保存答案:", this.answers);
  322. },
  323. // 获取答题时长(秒)
  324. getAnswerDuration() {
  325. const remainingTimeArr = this.remainingTime.split(":");
  326. const remainingSeconds = parseInt(remainingTimeArr[0]) * 60 + parseInt(remainingTimeArr[1]);
  327. return 30 - remainingSeconds;
  328. },
  329. // 提交当前答案
  330. async submitCurrentAnswer() {
  331. if (!this.currentAnswer || this.isSubmitting)
  332. return;
  333. try {
  334. this.isSubmitting = true;
  335. common_vendor.index.showLoading({
  336. title: "正在提交答案..."
  337. });
  338. let answerContent = "";
  339. let answerOptions = [];
  340. if (this.currentAnswer.questionType === 0) {
  341. answerContent = this.currentAnswer.answer;
  342. answerOptions = [];
  343. } else if (this.currentAnswer.questionType === 1 || this.currentAnswer.questionType === 3) {
  344. const selectedIndex = this.currentAnswer.answer;
  345. const selectedOption = this.currentQuestion.options[selectedIndex];
  346. console.log("selectedOption", selectedOption);
  347. const optionId = selectedOption.id ? selectedOption.id : selectedIndex;
  348. answerContent = optionId.toString();
  349. answerOptions = [optionId];
  350. } else if (this.currentAnswer.questionType === 2) {
  351. const selectedIndices = this.currentAnswer.answer;
  352. const selectedOptionIds = selectedIndices.map((index) => {
  353. const option = this.currentQuestion.options[index];
  354. return option.id ? option.id : index;
  355. });
  356. answerOptions = selectedOptionIds;
  357. }
  358. const submitData = {
  359. job_id: JSON.parse(common_vendor.index.getStorageSync("selectedJob")).id,
  360. applicant_id: JSON.parse(common_vendor.index.getStorageSync("userInfo")).id,
  361. question_id: this.currentAnswer.questionId,
  362. // answer_content: answerContent,
  363. answer_options: answerOptions,
  364. answer_duration: this.currentAnswer.answerDuration || 0,
  365. tenant_id: 1
  366. };
  367. console.log("提交数据:", submitData);
  368. const res2 = await this.$http.post(`${common_config.apiBaseUrl}/api/job/submit_answer`, submitData);
  369. console.log("提交答案响应:", res2);
  370. return res2;
  371. } catch (error) {
  372. console.error("提交答案失败:", error);
  373. common_vendor.index.showToast({
  374. title: "提交答案失败,请重试",
  375. icon: "none"
  376. });
  377. throw error;
  378. } finally {
  379. common_vendor.index.hideLoading();
  380. this.isSubmitting = false;
  381. }
  382. },
  383. // 修改 goToNextQuestion 方法,在切换到下一题时重置当前选项状态
  384. async goToNextQuestion() {
  385. this.showResult = false;
  386. this.selectedOption = null;
  387. this.selectedOptions = [];
  388. this.openQuestionAnswer = "";
  389. this.currentQuestionIndex++;
  390. this.currentScrollId = "question-" + this.currentQuestionIndex;
  391. if (this.questions[this.currentQuestionIndex]) {
  392. this.progressWidth = (this.currentQuestionIndex + 1) / this.questions.length * 100;
  393. this.resetTimer();
  394. this.playAiSpeaking();
  395. setTimeout(() => {
  396. this.pauseAiSpeaking();
  397. }, 2e3);
  398. return;
  399. }
  400. try {
  401. this.loading = true;
  402. await new Promise((resolve) => setTimeout(resolve, 1e3));
  403. const res2 = {
  404. /* 模拟的题目数据 */
  405. };
  406. if (res2) {
  407. const formattedQuestion = {
  408. id: res2.id || this.currentQuestionIndex + 1,
  409. text: res2.question || "未知问题",
  410. options: res2.options || [],
  411. correctAnswer: res2.correctAnswer || 0,
  412. isImportant: res2.is_system || false,
  413. explanation: res2.explanation || "",
  414. questionType: res2.question_form || 1,
  415. questionTypeName: res2.question_form_name || "单选题",
  416. correctAnswers: res2.correct_answers || [],
  417. difficulty: res2.difficulty || 1,
  418. difficultyName: res2.difficulty_name || "初级"
  419. };
  420. this.questions.push(formattedQuestion);
  421. }
  422. } catch (error) {
  423. console.error("获取题目详情失败:", error);
  424. common_vendor.index.showToast({
  425. title: "获取题目失败,请重试",
  426. icon: "none"
  427. });
  428. this.currentQuestionIndex--;
  429. } finally {
  430. this.loading = false;
  431. this.progressWidth = (this.currentQuestionIndex + 1) / this.questions.length * 100;
  432. this.resetTimer();
  433. }
  434. },
  435. toggleSettings() {
  436. common_vendor.index.showToast({
  437. title: "设置功能开发中",
  438. icon: "none"
  439. });
  440. },
  441. back(target) {
  442. if (this.timerInterval) {
  443. clearInterval(this.timerInterval);
  444. }
  445. if (target) {
  446. common_vendor.index.navigateTo({
  447. url: target
  448. });
  449. return;
  450. }
  451. },
  452. error(e) {
  453. console.error(e.detail);
  454. common_vendor.index.showToast({
  455. title: "相机启动失败,请检查权限设置",
  456. icon: "none"
  457. });
  458. },
  459. playAiSpeaking() {
  460. if (this.useVideo && this.aiVideoContext) {
  461. this.aiVideoContext.play();
  462. }
  463. if (this.digitalHumanUrl) {
  464. const speakText = this.currentQuestion ? this.currentQuestion.text : "";
  465. this.interactWithDigitalHuman(speakText);
  466. }
  467. },
  468. pauseAiSpeaking() {
  469. if (this.useVideo && this.aiVideoContext) {
  470. this.aiVideoContext.pause();
  471. }
  472. },
  473. // 修改 restartTest 方法,添加可选的跳转目标
  474. restartTest(target) {
  475. this.currentQuestionIndex = 0;
  476. this.score = 0;
  477. this.showEndModal = false;
  478. this.showResult = false;
  479. this.selectedOption = null;
  480. this.selectedOptions = [];
  481. this.resetTimer();
  482. if (target) {
  483. common_vendor.index.navigateTo({
  484. url: target
  485. });
  486. }
  487. },
  488. // 在methods中添加测试方法
  489. testEndScreen() {
  490. this.interviewCompleted = true;
  491. this.showEndModal = false;
  492. },
  493. // 初始化数字人
  494. initDigitalHuman() {
  495. this.digitalHumanUrl = "";
  496. },
  497. // 与数字人交互的方法
  498. interactWithDigitalHuman(message) {
  499. const webview = this.$mp.page.$getAppWebview().children()[0];
  500. if (webview) {
  501. webview.evalJS(`receiveMessage('${message}')`);
  502. }
  503. },
  504. // 添加 navigateToInterview 方法
  505. navigateToInterview() {
  506. this.showEndModal = false;
  507. common_vendor.index.navigateTo({
  508. url: "/pages/interview/interview",
  509. success: () => {
  510. console.log("成功跳转到interview页面");
  511. },
  512. fail: (err) => {
  513. console.error("跳转失败:", err);
  514. common_vendor.index.showToast({
  515. title: "跳转失败,请手动返回首页",
  516. icon: "none"
  517. });
  518. }
  519. });
  520. },
  521. // 添加 handleCameraError 方法
  522. handleCameraError(e) {
  523. console.error("相机错误:", e.detail);
  524. common_vendor.index.showToast({
  525. title: "相机启动失败,请检查权限设置",
  526. icon: "none"
  527. });
  528. },
  529. // 修改 isOptionSelected 方法,更精确地判断选项是否被选中
  530. isOptionSelected(question, qIndex, optionIndex) {
  531. const answer = this.answers.find((a) => a.questionId === question.id);
  532. if (qIndex === this.currentQuestionIndex) {
  533. if (question.questionType === 1 || question.questionType === 3) {
  534. return this.selectedOption === optionIndex;
  535. } else if (question.questionType === 2) {
  536. return this.selectedOptions.includes(optionIndex);
  537. }
  538. } else if (answer) {
  539. if (question.questionType === 1 || question.questionType === 3) {
  540. return answer.answer === optionIndex;
  541. } else if (question.questionType === 2 && Array.isArray(answer.answer)) {
  542. return answer.answer.includes(optionIndex);
  543. }
  544. }
  545. return false;
  546. }
  547. },
  548. // 添加生命周期钩子,确保在组件销毁时清除计时器
  549. beforeDestroy() {
  550. if (this.timerInterval) {
  551. clearInterval(this.timerInterval);
  552. }
  553. }
  554. };
  555. if (!Array) {
  556. const _component_uni_load_more = common_vendor.resolveComponent("uni-load-more");
  557. _component_uni_load_more();
  558. }
  559. function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  560. return common_vendor.e({
  561. a: !$data.digitalHumanUrl
  562. }, !$data.digitalHumanUrl ? {
  563. b: $data.mode,
  564. c: common_vendor.o((...args) => $options.handleCameraError && $options.handleCameraError(...args))
  565. } : $data.digitalHumanUrl ? {
  566. e: $data.digitalHumanUrl
  567. } : {
  568. f: common_assets._imports_0$1
  569. }, {
  570. d: $data.digitalHumanUrl,
  571. g: common_vendor.f($data.questions, (question, qIndex, i0) => {
  572. return common_vendor.e({
  573. a: common_vendor.t(qIndex + 1),
  574. b: common_vendor.t(question.questionTypeName),
  575. c: common_vendor.t(question.text),
  576. d: question.questionType === 3 && question.imageUrl
  577. }, question.questionType === 3 && question.imageUrl ? {
  578. e: question.imageUrl
  579. } : {}, {
  580. f: question.questionType !== 0
  581. }, question.questionType !== 0 ? {
  582. g: common_vendor.f(question.options, (option, index, i1) => {
  583. return {
  584. a: common_vendor.t(option.option_text || (typeof option === "string" ? option : JSON.stringify(option))),
  585. b: index,
  586. c: $options.isOptionSelected(question, qIndex, index) ? 1 : "",
  587. d: common_vendor.o(($event) => qIndex === $data.currentQuestionIndex && $options.selectOption(index), index)
  588. };
  589. }),
  590. h: qIndex !== $data.currentQuestionIndex ? 1 : ""
  591. } : {}, {
  592. i: qIndex === $data.currentQuestionIndex && question.questionType === 2
  593. }, qIndex === $data.currentQuestionIndex && question.questionType === 2 ? {
  594. j: common_vendor.t("提交答案"),
  595. k: common_vendor.o(($event) => $options.nextQuestion(), question.id),
  596. l: $data.selectedOptions.length === 0
  597. } : {}, {
  598. m: question.id,
  599. n: "question-" + qIndex
  600. });
  601. }),
  602. h: common_vendor.t($data.questions.length),
  603. i: $data.currentScrollId,
  604. j: $data.showEndModal
  605. }, $data.showEndModal ? {
  606. k: common_vendor.o((...args) => $options.navigateToInterview && $options.navigateToInterview(...args))
  607. } : {}, {
  608. l: $data.interviewCompleted
  609. }, $data.interviewCompleted ? {
  610. m: common_assets._imports_0$1,
  611. n: common_vendor.o((...args) => $options.back && $options.back(...args))
  612. } : {}, {
  613. o: $data.loading
  614. }, $data.loading ? {
  615. p: common_vendor.p({
  616. status: "loading",
  617. contentText: {
  618. contentdown: "加载中..."
  619. }
  620. })
  621. } : {}, {
  622. q: !$data.loading && $data.loadError
  623. }, !$data.loading && $data.loadError ? {
  624. r: common_vendor.o((...args) => $options.fetchInterviewData && $options.fetchInterviewData(...args))
  625. } : {});
  626. }
  627. const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
  628. wx.createPage(MiniProgramPage);