camera.js 19 KB

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