camera.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  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, page: 1, limit: 999 });
  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. answerOptions = selectedOptionIds;
  341. }
  342. const submitData = {
  343. job_id: JSON.parse(common_vendor.index.getStorageSync("selectedJob")).id,
  344. applicant_id: JSON.parse(common_vendor.index.getStorageSync("userInfo")).id,
  345. question_id: this.currentAnswer.questionId,
  346. // answer_content: answerContent,
  347. answer_options: answerOptions,
  348. answer_duration: this.currentAnswer.answerDuration || 0,
  349. tenant_id: 1
  350. };
  351. console.log("提交数据:", submitData);
  352. const res2 = await this.$http.post(`${common_config.apiBaseUrl}/api/job/submit_answer`, submitData);
  353. console.log("提交答案响应:", res2);
  354. return res2;
  355. } catch (error) {
  356. console.error("提交答案失败:", error);
  357. common_vendor.index.showToast({
  358. title: "提交答案失败,请重试",
  359. icon: "none"
  360. });
  361. throw error;
  362. } finally {
  363. common_vendor.index.hideLoading();
  364. this.isSubmitting = false;
  365. }
  366. },
  367. // 修改 goToNextQuestion 方法,添加 async 关键字
  368. async goToNextQuestion() {
  369. this.showResult = false;
  370. this.selectedOption = null;
  371. this.selectedOptions = [];
  372. this.openQuestionAnswer = "";
  373. this.currentQuestionIndex++;
  374. if (this.questions[this.currentQuestionIndex]) {
  375. this.progressWidth = (this.currentQuestionIndex + 1) / this.questions.length * 100;
  376. this.resetTimer();
  377. this.playAiSpeaking();
  378. setTimeout(() => {
  379. this.pauseAiSpeaking();
  380. }, 2e3);
  381. return;
  382. }
  383. try {
  384. this.loading = true;
  385. await new Promise((resolve) => setTimeout(resolve, 1e3));
  386. const res2 = {
  387. /* 模拟的题目数据 */
  388. };
  389. if (res2) {
  390. const formattedQuestion = {
  391. id: res2.id || this.currentQuestionIndex + 1,
  392. text: res2.question || "未知问题",
  393. options: res2.options || [],
  394. correctAnswer: res2.correctAnswer || 0,
  395. isImportant: res2.is_system || false,
  396. explanation: res2.explanation || "",
  397. questionType: res2.question_form || 1,
  398. questionTypeName: res2.question_form_name || "单选题",
  399. correctAnswers: res2.correct_answers || [],
  400. difficulty: res2.difficulty || 1,
  401. difficultyName: res2.difficulty_name || "初级"
  402. };
  403. this.questions.push(formattedQuestion);
  404. }
  405. } catch (error) {
  406. console.error("获取题目详情失败:", error);
  407. common_vendor.index.showToast({
  408. title: "获取题目失败,请重试",
  409. icon: "none"
  410. });
  411. this.currentQuestionIndex--;
  412. } finally {
  413. this.loading = false;
  414. this.progressWidth = (this.currentQuestionIndex + 1) / this.questions.length * 100;
  415. this.resetTimer();
  416. }
  417. },
  418. toggleSettings() {
  419. common_vendor.index.showToast({
  420. title: "设置功能开发中",
  421. icon: "none"
  422. });
  423. },
  424. back(target) {
  425. if (this.timerInterval) {
  426. clearInterval(this.timerInterval);
  427. }
  428. if (target) {
  429. common_vendor.index.navigateTo({
  430. url: target
  431. });
  432. return;
  433. }
  434. },
  435. error(e) {
  436. console.error(e.detail);
  437. common_vendor.index.showToast({
  438. title: "相机启动失败,请检查权限设置",
  439. icon: "none"
  440. });
  441. },
  442. playAiSpeaking() {
  443. if (this.useVideo && this.aiVideoContext) {
  444. this.aiVideoContext.play();
  445. }
  446. if (this.digitalHumanUrl) {
  447. const speakText = this.currentQuestion ? this.currentQuestion.text : "";
  448. this.interactWithDigitalHuman(speakText);
  449. }
  450. },
  451. pauseAiSpeaking() {
  452. if (this.useVideo && this.aiVideoContext) {
  453. this.aiVideoContext.pause();
  454. }
  455. },
  456. // 修改 restartTest 方法,添加可选的跳转目标
  457. restartTest(target) {
  458. this.currentQuestionIndex = 0;
  459. this.score = 0;
  460. this.showEndModal = false;
  461. this.showResult = false;
  462. this.selectedOption = null;
  463. this.selectedOptions = [];
  464. this.resetTimer();
  465. if (target) {
  466. common_vendor.index.navigateTo({
  467. url: target
  468. });
  469. }
  470. },
  471. // 在methods中添加测试方法
  472. testEndScreen() {
  473. this.interviewCompleted = true;
  474. this.showEndModal = false;
  475. },
  476. // 初始化数字人
  477. initDigitalHuman() {
  478. this.digitalHumanUrl = "";
  479. },
  480. // 与数字人交互的方法
  481. interactWithDigitalHuman(message) {
  482. const webview = this.$mp.page.$getAppWebview().children()[0];
  483. if (webview) {
  484. webview.evalJS(`receiveMessage('${message}')`);
  485. }
  486. },
  487. // 添加 navigateToInterview 方法
  488. navigateToInterview() {
  489. this.showEndModal = false;
  490. common_vendor.index.navigateTo({
  491. url: "/pages/interview/interview",
  492. success: () => {
  493. console.log("成功跳转到interview页面");
  494. },
  495. fail: (err) => {
  496. console.error("跳转失败:", err);
  497. common_vendor.index.showToast({
  498. title: "跳转失败,请手动返回首页",
  499. icon: "none"
  500. });
  501. }
  502. });
  503. },
  504. // 添加 handleCameraError 方法
  505. handleCameraError(e) {
  506. console.error("相机错误:", e.detail);
  507. common_vendor.index.showToast({
  508. title: "相机启动失败,请检查权限设置",
  509. icon: "none"
  510. });
  511. }
  512. },
  513. // 添加生命周期钩子,确保在组件销毁时清除计时器
  514. beforeDestroy() {
  515. if (this.timerInterval) {
  516. clearInterval(this.timerInterval);
  517. }
  518. }
  519. };
  520. if (!Array) {
  521. const _component_uni_load_more = common_vendor.resolveComponent("uni-load-more");
  522. _component_uni_load_more();
  523. }
  524. function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  525. return common_vendor.e({
  526. a: !$data.digitalHumanUrl
  527. }, !$data.digitalHumanUrl ? {
  528. b: $data.mode,
  529. c: common_vendor.o((...args) => $options.handleCameraError && $options.handleCameraError(...args))
  530. } : $data.digitalHumanUrl ? {
  531. e: $data.digitalHumanUrl
  532. } : {
  533. f: common_assets._imports_0$1
  534. }, {
  535. d: $data.digitalHumanUrl,
  536. g: common_vendor.t($data.currentQuestionIndex + 1),
  537. h: common_vendor.t($data.questions.length),
  538. i: common_vendor.t($options.currentQuestion.questionTypeName),
  539. j: common_vendor.t($options.currentQuestion.text),
  540. k: $options.currentQuestion.questionType === 3 && $options.currentQuestion.imageUrl
  541. }, $options.currentQuestion.questionType === 3 && $options.currentQuestion.imageUrl ? {
  542. l: $options.currentQuestion.imageUrl
  543. } : {}, {
  544. m: $options.currentQuestion.questionType !== 0
  545. }, $options.currentQuestion.questionType !== 0 ? {
  546. n: common_vendor.f($options.currentQuestion.options, (option, index, i0) => {
  547. return {
  548. a: common_vendor.t(option.option_text || (typeof option === "string" ? option : JSON.stringify(option))),
  549. b: index,
  550. c: ($options.currentQuestion.questionType === 1 || $options.currentQuestion.questionType === 3 ? $data.selectedOption === index : $data.selectedOptions.includes(index)) ? 1 : "",
  551. d: common_vendor.o(($event) => $options.selectOption(index), index)
  552. };
  553. })
  554. } : {}, {
  555. o: common_vendor.t($data.remainingTime),
  556. p: common_vendor.t("进入下一题"),
  557. q: common_vendor.o(($event) => $options.nextQuestion(_ctx.option)),
  558. r: $options.currentQuestion.questionType === 0 && $data.openQuestionAnswer.trim() === "" || $options.currentQuestion.questionType === 1 && $data.selectedOption === null || $options.currentQuestion.questionType === 2 && $data.selectedOptions.length === 0,
  559. s: $data.showEndModal
  560. }, $data.showEndModal ? {
  561. t: common_vendor.o((...args) => $options.navigateToInterview && $options.navigateToInterview(...args))
  562. } : {}, {
  563. v: $data.interviewCompleted
  564. }, $data.interviewCompleted ? {
  565. w: common_assets._imports_0$1,
  566. x: common_vendor.o((...args) => $options.back && $options.back(...args))
  567. } : {}, {
  568. y: $data.loading
  569. }, $data.loading ? {
  570. z: common_vendor.p({
  571. status: "loading",
  572. contentText: {
  573. contentdown: "加载中..."
  574. }
  575. })
  576. } : {}, {
  577. A: !$data.loading && $data.loadError
  578. }, !$data.loading && $data.loadError ? {
  579. B: common_vendor.o((...args) => $options.fetchInterviewData && $options.fetchInterviewData(...args))
  580. } : {});
  581. }
  582. const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
  583. wx.createPage(MiniProgramPage);