camera.js 20 KB

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