camera.js 20 KB

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