camera.js 20 KB

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