camera.js 18 KB

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