camera.js 20 KB

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