camera.js 19 KB

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