camera.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  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. this.showEndModal = true;
  232. return;
  233. }
  234. this.goToNextQuestion();
  235. },
  236. // 保存当前题目的答案
  237. saveAnswer() {
  238. let answer;
  239. if (this.currentQuestion.questionType === 0) {
  240. answer = {
  241. questionId: this.currentQuestion.id,
  242. questionType: this.currentQuestion.questionType,
  243. answer: this.openQuestionAnswer,
  244. answerDuration: this.getAnswerDuration()
  245. // 添加答题时长
  246. };
  247. } else {
  248. answer = {
  249. questionId: this.currentQuestion.id,
  250. questionType: this.currentQuestion.questionType,
  251. answer: this.currentQuestion.questionType === 1 ? this.selectedOption : this.selectedOptions,
  252. answerDuration: this.getAnswerDuration()
  253. // 添加答题时长
  254. };
  255. }
  256. const existingIndex = this.answers.findIndex((a) => a.questionId === answer.questionId);
  257. if (existingIndex > -1) {
  258. this.answers[existingIndex] = answer;
  259. } else {
  260. this.answers.push(answer);
  261. }
  262. this.currentAnswer = answer;
  263. console.log("已保存答案:", this.answers);
  264. },
  265. // 获取答题时长(秒)
  266. getAnswerDuration() {
  267. const remainingTimeArr = this.remainingTime.split(":");
  268. const remainingSeconds = parseInt(remainingTimeArr[0]) * 60 + parseInt(remainingTimeArr[1]);
  269. return 30 - remainingSeconds;
  270. },
  271. // 提交当前答案
  272. async submitCurrentAnswer() {
  273. if (!this.currentAnswer)
  274. return;
  275. try {
  276. common_vendor.index.showLoading({
  277. title: "正在提交答案..."
  278. });
  279. let answerContent = "";
  280. if (this.currentAnswer.questionType === 0) {
  281. answerContent = this.currentAnswer.answer;
  282. } else if (this.currentAnswer.questionType === 1) {
  283. const selectedIndex = this.currentAnswer.answer;
  284. const selectedOption = this.currentQuestion.options[selectedIndex];
  285. answerContent = selectedOption.id ? selectedOption.id.toString() : selectedIndex.toString();
  286. } else if (this.currentAnswer.questionType === 2) {
  287. const selectedIndices = this.currentAnswer.answer;
  288. const selectedOptionIds = selectedIndices.map((index) => {
  289. const option = this.currentQuestion.options[index];
  290. return option.id ? option.id : index;
  291. });
  292. answerContent = selectedOptionIds.join(",");
  293. }
  294. const submitData = {
  295. application_id: 1,
  296. // 或者使用其他合适的ID
  297. question_id: this.currentAnswer.questionId,
  298. answer_content: answerContent,
  299. answer_duration: this.currentAnswer.answerDuration || 0,
  300. // 如果需要tenant_id,请在这里添加
  301. tenant_id: 1
  302. };
  303. console.log("提交数据:", submitData);
  304. const res2 = await this.$http.post("http://192.168.66.187:8083/api/job/submit_answer", submitData);
  305. console.log("提交答案响应:", res2);
  306. common_vendor.index.hideLoading();
  307. return res2;
  308. } catch (error) {
  309. console.error("提交答案失败:", error);
  310. common_vendor.index.hideLoading();
  311. common_vendor.index.showToast({
  312. title: "提交答案失败,请重试",
  313. icon: "none"
  314. });
  315. throw error;
  316. }
  317. },
  318. // 修改 goToNextQuestion 方法,添加 async 关键字
  319. async goToNextQuestion() {
  320. this.showResult = false;
  321. this.selectedOption = null;
  322. this.selectedOptions = [];
  323. this.openQuestionAnswer = "";
  324. this.currentQuestionIndex++;
  325. if (this.questions[this.currentQuestionIndex]) {
  326. this.progressWidth = (this.currentQuestionIndex + 1) / this.questions.length * 100;
  327. this.resetTimer();
  328. this.playAiSpeaking();
  329. setTimeout(() => {
  330. this.pauseAiSpeaking();
  331. }, 2e3);
  332. return;
  333. }
  334. try {
  335. this.loading = true;
  336. await new Promise((resolve) => setTimeout(resolve, 1e3));
  337. const res2 = {
  338. /* 模拟的题目数据 */
  339. };
  340. if (res2) {
  341. const formattedQuestion = {
  342. id: res2.id || this.currentQuestionIndex + 1,
  343. text: res2.question || "未知问题",
  344. options: res2.options || [],
  345. correctAnswer: res2.correctAnswer || 0,
  346. isImportant: res2.is_system || false,
  347. explanation: res2.explanation || "",
  348. questionType: res2.question_form || 1,
  349. questionTypeName: res2.question_form_name || "单选题",
  350. correctAnswers: res2.correct_answers || [],
  351. difficulty: res2.difficulty || 1,
  352. difficultyName: res2.difficulty_name || "初级"
  353. };
  354. this.questions.push(formattedQuestion);
  355. }
  356. } catch (error) {
  357. console.error("获取题目详情失败:", error);
  358. common_vendor.index.showToast({
  359. title: "获取题目失败,请重试",
  360. icon: "none"
  361. });
  362. this.currentQuestionIndex--;
  363. } finally {
  364. this.loading = false;
  365. this.progressWidth = (this.currentQuestionIndex + 1) / this.questions.length * 100;
  366. this.resetTimer();
  367. }
  368. },
  369. toggleSettings() {
  370. common_vendor.index.showToast({
  371. title: "设置功能开发中",
  372. icon: "none"
  373. });
  374. },
  375. back() {
  376. if (this.timerInterval) {
  377. clearInterval(this.timerInterval);
  378. }
  379. try {
  380. const pages = getCurrentPages();
  381. if (pages.length > 1) {
  382. common_vendor.index.reLaunch({
  383. url: "/pages/index/index"
  384. });
  385. } else {
  386. common_vendor.index.reLaunch({
  387. url: "/pages/index/index"
  388. });
  389. }
  390. } catch (e) {
  391. console.error("导航错误:", e);
  392. common_vendor.index.reLaunch({
  393. url: "/pages/index/index"
  394. });
  395. }
  396. },
  397. error(e) {
  398. console.error(e.detail);
  399. common_vendor.index.showToast({
  400. title: "相机启动失败,请检查权限设置",
  401. icon: "none"
  402. });
  403. },
  404. playAiSpeaking() {
  405. if (this.useVideo && this.aiVideoContext) {
  406. this.aiVideoContext.play();
  407. }
  408. if (this.digitalHumanUrl) {
  409. const speakText = this.currentQuestion ? this.currentQuestion.text : "";
  410. this.interactWithDigitalHuman(speakText);
  411. }
  412. },
  413. pauseAiSpeaking() {
  414. if (this.useVideo && this.aiVideoContext) {
  415. this.aiVideoContext.pause();
  416. }
  417. },
  418. // 重新开始测试
  419. restartTest() {
  420. this.currentQuestionIndex = 0;
  421. this.score = 0;
  422. this.showEndModal = false;
  423. this.showResult = false;
  424. this.selectedOption = null;
  425. this.selectedOptions = [];
  426. this.resetTimer();
  427. },
  428. // 在methods中添加测试方法
  429. testEndScreen() {
  430. this.interviewCompleted = true;
  431. this.showEndModal = false;
  432. },
  433. // 初始化数字人
  434. initDigitalHuman() {
  435. this.digitalHumanUrl = "";
  436. },
  437. // 与数字人交互的方法
  438. interactWithDigitalHuman(message) {
  439. const webview = this.$mp.page.$getAppWebview().children()[0];
  440. if (webview) {
  441. webview.evalJS(`receiveMessage('${message}')`);
  442. }
  443. }
  444. },
  445. // 添加生命周期钩子,确保在组件销毁时清除计时器
  446. beforeDestroy() {
  447. if (this.timerInterval) {
  448. clearInterval(this.timerInterval);
  449. }
  450. }
  451. };
  452. if (!Array) {
  453. const _component_uni_load_more = common_vendor.resolveComponent("uni-load-more");
  454. _component_uni_load_more();
  455. }
  456. function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  457. return common_vendor.e({
  458. a: $data.digitalHumanUrl
  459. }, $data.digitalHumanUrl ? {
  460. b: $data.digitalHumanUrl
  461. } : {
  462. c: common_assets._imports_0
  463. }, {
  464. d: common_vendor.t($data.currentQuestionIndex + 1),
  465. e: common_vendor.t($options.currentQuestion.id),
  466. f: common_vendor.t($data.questions.length),
  467. g: $options.currentQuestion.isImportant
  468. }, $options.currentQuestion.isImportant ? {} : {}, {
  469. h: common_vendor.t($options.currentQuestion.questionTypeName),
  470. i: common_vendor.t($options.currentQuestion.text),
  471. j: $options.currentQuestion.questionType == 0
  472. }, $options.currentQuestion.questionType == 0 ? {
  473. k: $data.openQuestionAnswer,
  474. l: common_vendor.o(($event) => $data.openQuestionAnswer = $event.detail.value),
  475. m: common_vendor.t($data.openQuestionAnswer.length)
  476. } : {
  477. n: common_vendor.f($options.currentQuestion.options, (option, index, i0) => {
  478. return {
  479. a: common_vendor.t(option.option_text || (typeof option === "string" ? option : JSON.stringify(option))),
  480. b: index,
  481. c: ($options.currentQuestion.questionType === 1 ? $data.selectedOption === index : $data.selectedOptions.includes(index)) ? 1 : "",
  482. d: $data.showResult && ($options.currentQuestion.questionType === 1 ? index === $options.currentQuestion.correctAnswer : $options.currentQuestion.correctAnswers.includes(index)) ? 1 : "",
  483. e: $data.showResult && ($options.currentQuestion.questionType === 1 ? $data.selectedOption === index && index !== $options.currentQuestion.correctAnswer : $data.selectedOptions.includes(index) && !$options.currentQuestion.correctAnswers.includes(index)) ? 1 : "",
  484. f: common_vendor.o(($event) => $options.selectOption(index), index)
  485. };
  486. }),
  487. o: common_vendor.t($options.currentQuestion.questionType === 1 ? "●" : "☐")
  488. }, {
  489. p: common_vendor.t($data.showResult ? "下一题" : "提交答案"),
  490. q: common_vendor.o(($event) => $options.nextQuestion(_ctx.option)),
  491. r: $options.currentQuestion.questionType === 0 && $data.openQuestionAnswer.trim() === "" || $options.currentQuestion.questionType === 1 && $data.selectedOption === null || $options.currentQuestion.questionType === 2 && $data.selectedOptions.length === 0,
  492. s: $data.showEndModal
  493. }, $data.showEndModal ? {
  494. t: common_vendor.o((...args) => $options.restartTest && $options.restartTest(...args)),
  495. v: common_vendor.o((...args) => $options.back && $options.back(...args))
  496. } : {}, {
  497. w: $data.interviewCompleted
  498. }, $data.interviewCompleted ? {
  499. x: common_assets._imports_0,
  500. y: common_vendor.o((...args) => $options.back && $options.back(...args))
  501. } : {}, {
  502. z: $data.loading
  503. }, $data.loading ? {
  504. A: common_vendor.p({
  505. status: "loading",
  506. contentText: {
  507. contentdown: "加载中..."
  508. }
  509. })
  510. } : {}, {
  511. B: !$data.loading && $data.loadError
  512. }, !$data.loading && $data.loadError ? {
  513. C: common_vendor.o((...args) => $options.fetchInterviewData && $options.fetchInterviewData(...args))
  514. } : {});
  515. }
  516. const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
  517. wx.createPage(MiniProgramPage);