camera.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970
  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. currentScrollId: "question-0",
  54. // 当前滚动到的问题ID
  55. questionGroups: [],
  56. // 存储分组后的题目
  57. currentGroupIndex: 0,
  58. // 当前显示的组索引
  59. showContinueButton: false,
  60. // 是否显示继续按钮
  61. scrollToBottom: false,
  62. // 新增:是否需要滚动到底部
  63. scrollTop: 0,
  64. // 控制 scroll-view 的滚动位置
  65. _clickTimer: null,
  66. _isClicking: false,
  67. showPromptModal: false,
  68. // 添加新的数据属性
  69. personDetectionSocket: null,
  70. // WebSocket对象
  71. personDetectionInterval: null,
  72. // 定时器对象
  73. showCameraWarning: false,
  74. // 添加新的数据属性
  75. showPageWarning: false,
  76. // 添加新的数据属性
  77. statusBarHeight: 0,
  78. positionConfig: {}
  79. };
  80. },
  81. computed: {
  82. currentQuestion() {
  83. console.log(this.questions[this.currentQuestionIndex]);
  84. return this.questions[this.currentQuestionIndex];
  85. }
  86. },
  87. onLoad(options) {
  88. if (options && options.id) {
  89. this.interviewId = options.id;
  90. this.fetchInterviewData();
  91. } else {
  92. this.fetchInterviewList();
  93. }
  94. this.statusBarHeight = common_vendor.index.getSystemInfoSync().statusBarHeight || 0;
  95. this.positionConfig = JSON.parse(common_vendor.index.getStorageSync("configData"));
  96. },
  97. onReady() {
  98. this.cameraContext = common_vendor.index.createCameraContext();
  99. if (this.useVideo) {
  100. this.aiVideoContext = common_vendor.index.createVideoContext("aiInterviewer");
  101. }
  102. this.initDigitalHuman();
  103. },
  104. mounted() {
  105. this.positionConfig = JSON.parse(common_vendor.index.getStorageSync("configData"));
  106. common_vendor.index.onUserCaptureScreen(() => {
  107. console.log("User captured screen");
  108. this.screenCaptureCount++;
  109. if (this.screenCaptureCount === 1) {
  110. common_vendor.index.showModal({
  111. title: "Warning",
  112. content: "检测到屏幕截图。如果你再次捕捉屏幕,你的面试将无效.",
  113. showCancel: false,
  114. confirmText: "OK"
  115. });
  116. } else if (this.screenCaptureCount >= 2) {
  117. common_vendor.index.showModal({
  118. title: "Interview Invalid",
  119. content: "由于多次屏幕截图,您的面试已无效。请联系招聘人员寻求帮助.",
  120. showCancel: false,
  121. confirmText: "OK",
  122. success: () => {
  123. this.invalidateInterview();
  124. }
  125. });
  126. }
  127. });
  128. },
  129. beforeDestroy() {
  130. this.cleanupPersonDetectionWebSocket();
  131. },
  132. methods: {
  133. initPersonDetectionWebSocket() {
  134. if (this.personDetectionSocket) {
  135. this.cleanupPersonDetectionWebSocket();
  136. }
  137. try {
  138. this.personDetectionSocket = common_vendor.index.connectSocket({
  139. url: `${common_config.personDetectionWsUrl}/ws/interview-room/room_${common_vendor.index.getStorageSync("appId")}/${common_vendor.index.getStorageSync("appId")}/`,
  140. success: () => {
  141. console.log("WebSocket connection initiated");
  142. },
  143. fail: (error) => {
  144. console.error("WebSocket connection failed:", error);
  145. }
  146. });
  147. this.personDetectionSocket.onOpen(() => {
  148. console.log("WebSocket connection opened");
  149. this.startPersonDetectionInterval();
  150. });
  151. this.personDetectionSocket.onError((error) => {
  152. console.error("WebSocket error:", error);
  153. this.cleanupPersonDetectionWebSocket();
  154. });
  155. this.personDetectionSocket.onClose(() => {
  156. console.log("WebSocket connection closed");
  157. this.cleanupPersonDetectionWebSocket();
  158. });
  159. this.personDetectionSocket.onMessage((res2) => {
  160. try {
  161. const data = JSON.parse(res2.data);
  162. console.log(data);
  163. if (data.type === "person_detection_result") {
  164. this.handlePersonDetectionResult(data);
  165. }
  166. } catch (error) {
  167. console.error("Error parsing WebSocket message:", error);
  168. }
  169. });
  170. } catch (error) {
  171. console.error("Error initializing WebSocket:", error);
  172. this.cleanupPersonDetectionWebSocket();
  173. }
  174. },
  175. startPersonDetectionInterval() {
  176. if (this.personDetectionInterval) {
  177. clearInterval(this.personDetectionInterval);
  178. }
  179. this.personDetectionInterval = setInterval(() => {
  180. if (this.personDetectionSocket && this.cameraContext) {
  181. this.cameraContext.takePhoto({
  182. quality: "low",
  183. success: (res2) => {
  184. const tempFilePath = res2.tempImagePath;
  185. common_vendor.index.getFileSystemManager().readFile({
  186. filePath: tempFilePath,
  187. encoding: "base64",
  188. success: (res3) => {
  189. const base64Image = res3.data;
  190. this.personDetectionSocket.send({
  191. data: JSON.stringify({
  192. type: "person_detection",
  193. image_data: base64Image
  194. })
  195. });
  196. },
  197. fail: (error) => {
  198. console.error("Error reading image file:", error);
  199. }
  200. });
  201. },
  202. fail: (error) => {
  203. console.error("Error taking photo:", error);
  204. }
  205. });
  206. }
  207. }, 3e3);
  208. },
  209. cleanupPersonDetectionWebSocket() {
  210. if (this.personDetectionInterval) {
  211. clearInterval(this.personDetectionInterval);
  212. this.personDetectionInterval = null;
  213. }
  214. if (this.personDetectionSocket) {
  215. try {
  216. this.personDetectionSocket.close();
  217. } catch (error) {
  218. console.error("Error closing WebSocket:", error);
  219. }
  220. this.personDetectionSocket = null;
  221. }
  222. },
  223. handlePersonDetectionResult(data) {
  224. console.log(data.data.detection.has_person);
  225. console.log(data.data.identity.status);
  226. if (data.data.identity.status !== "identity_verified") {
  227. this.showPageWarning = true;
  228. common_vendor.index.showToast({
  229. title: data.data.identity.message,
  230. icon: "none",
  231. duration: 3e3
  232. });
  233. common_vendor.index.vibrateLong({
  234. success: function() {
  235. console.log("Vibration successful");
  236. },
  237. fail: function(err) {
  238. console.error("Vibration failed:", err);
  239. }
  240. });
  241. setTimeout(() => {
  242. this.showPageWarning = false;
  243. }, 3e3);
  244. }
  245. },
  246. // 添加作废面试的方法
  247. invalidateInterview() {
  248. if (this.isRecording) {
  249. this.stopRecordingAnswer();
  250. }
  251. if (this.recordingTimer) {
  252. clearInterval(this.recordingTimer);
  253. }
  254. this.isRecording = false;
  255. this.showStopRecordingButton = false;
  256. this.showStartRecordingButton = false;
  257. setTimeout(() => {
  258. common_vendor.index.switchTab({
  259. url: "/pages/index/index"
  260. });
  261. }, 500);
  262. const systemInfo = common_vendor.index.getSystemInfoSync();
  263. const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith("mp-");
  264. if (isMiniProgram) {
  265. this.initPersonDetectionWebSocket();
  266. }
  267. },
  268. // 获取面试列表
  269. async fetchInterviewList() {
  270. try {
  271. this.loading = true;
  272. const res2 = await api_user.getQuestions({
  273. position_id: JSON.parse(common_vendor.index.getStorageSync("selectedJob")).id,
  274. page: 1,
  275. limit: 999,
  276. tenant_id: JSON.parse(common_vendor.index.getStorageSync("userInfo")).tenant_id || 1
  277. });
  278. console.log(res2.items);
  279. this.interviewId = res2.items;
  280. this.fetchInterviewData(res2.items);
  281. } catch (error) {
  282. console.error("获取面试列表失败:", error);
  283. this.handleLoadError("获取面试列表失败");
  284. }
  285. },
  286. // 获取面试详情数据
  287. async fetchInterviewData(data) {
  288. try {
  289. this.loading = true;
  290. if (data && Array.isArray(data)) {
  291. const formattedQuestions = data.filter((q) => q.question_form !== 0).map((q, index) => ({
  292. id: q.id || index + 1,
  293. text: q.question || "未知问题",
  294. options: q.options || [],
  295. correctAnswer: q.correctAnswer || 0,
  296. isImportant: q.is_system || false,
  297. explanation: q.explanation || "",
  298. questionType: q.question_form,
  299. questionTypeName: q.question_form_name || "单选题",
  300. is_required_correct: q.is_required_correct || false,
  301. correctAnswers: q.correct_answers || [],
  302. difficulty: q.difficulty || 1,
  303. difficultyName: q.difficulty_name || "初级",
  304. imageUrl: q.question_image_url || "",
  305. // 添加图片URL字段
  306. category: q.category || 0
  307. // 添加 category 字段
  308. }));
  309. this.processQuestionGroups(formattedQuestions);
  310. } else {
  311. this.processInterviewData(res);
  312. }
  313. console.log(this.questions);
  314. this.totalQuestions = this.questions.length;
  315. } catch (error) {
  316. console.error("获取面试详情失败:", error);
  317. this.handleLoadError("获取面试详情失败");
  318. } finally {
  319. this.loading = false;
  320. }
  321. },
  322. // 处理面试数据
  323. processInterviewData(data) {
  324. this.questions = [];
  325. if (data && data.question_form !== 0) {
  326. const formattedQuestion = {
  327. id: data.id || 1,
  328. text: data.question || "未知问题",
  329. options: data.options || [],
  330. correctAnswer: data.correctAnswer || 0,
  331. isImportant: data.is_system || false,
  332. explanation: data.explanation || "",
  333. questionType: data.question_form,
  334. // 1-单选题,2-多选题,3-看图答题
  335. questionTypeName: data.question_form_name || "单选题",
  336. is_required_correct: data.is_required_correct || false,
  337. correctAnswers: data.correct_answers || [],
  338. difficulty: data.difficulty || 1,
  339. difficultyName: data.difficulty_name || "初级",
  340. imageUrl: data.question_image_url || ""
  341. // 添加图片URL字段
  342. };
  343. this.questions.push(formattedQuestion);
  344. this.totalQuestions = this.questions.length;
  345. } else {
  346. this.handleLoadError("没有可用的选择题");
  347. }
  348. },
  349. // 处理加载错误
  350. handleLoadError(message) {
  351. this.loadError = true;
  352. this.loading = false;
  353. this.errorMessage = message || "加载失败";
  354. common_vendor.index.showToast({
  355. title: message || "加载失败",
  356. icon: "none",
  357. duration: 2e3
  358. });
  359. },
  360. startTimer() {
  361. if (this.questions.length === 0)
  362. return;
  363. this.questionStartTime = /* @__PURE__ */ new Date();
  364. let seconds = 60;
  365. this.remainingTime = `01:00`;
  366. this.timerInterval = setInterval(() => {
  367. seconds--;
  368. if (seconds <= 0) {
  369. clearInterval(this.timerInterval);
  370. if (!this.showResult) {
  371. this.checkAnswer();
  372. setTimeout(() => {
  373. if (this.currentQuestionIndex < this.questions.length - 1) {
  374. this.goToNextQuestion();
  375. } else {
  376. common_vendor.index.navigateTo({
  377. url: "/pages/posture-guide/posture-guide"
  378. });
  379. }
  380. }, 1500);
  381. }
  382. }
  383. const min = Math.floor(seconds / 60).toString().padStart(2, "0");
  384. const sec = (seconds % 60).toString().padStart(2, "0");
  385. this.remainingTime = `${min}:${sec}`;
  386. }, 1e3);
  387. },
  388. resetTimer() {
  389. clearInterval(this.timerInterval);
  390. },
  391. selectOption(index) {
  392. if (this.currentQuestionIndex === this.questions.length - 1 && this.showContinueButton) {
  393. common_vendor.index.showToast({
  394. title: "该题目已完成,请继续下一部分",
  395. icon: "none",
  396. duration: 2e3
  397. });
  398. return;
  399. }
  400. if (this.showResult)
  401. return;
  402. console.log("selectOption", index);
  403. if (this.currentQuestion.questionType === 2) {
  404. const optionIndex = this.selectedOptions.indexOf(index);
  405. if (optionIndex > -1) {
  406. this.selectedOptions.splice(optionIndex, 1);
  407. } else {
  408. this.selectedOptions.push(index);
  409. }
  410. } else if (this.currentQuestion.questionType === 1 || this.currentQuestion.questionType === 3 || this.currentQuestion.questionType === 4) {
  411. this.selectedOption = index;
  412. this.playAiSpeaking();
  413. setTimeout(() => {
  414. this.checkAnswer();
  415. this.saveAnswer();
  416. this.submitCurrentAnswer().then(() => {
  417. setTimeout(() => {
  418. this.goToNextQuestion();
  419. }, 800);
  420. }).catch((error) => {
  421. console.error("提交答案失败:", error);
  422. setTimeout(() => {
  423. this.goToNextQuestion();
  424. }, 1e3);
  425. });
  426. }, 500);
  427. } else {
  428. this.selectedOption = index;
  429. const answer = {
  430. questionId: this.currentQuestion.id,
  431. questionType: this.currentQuestion.questionType,
  432. answer: index
  433. };
  434. const existingIndex = this.answers.findIndex((a) => a.questionId === answer.questionId);
  435. if (existingIndex > -1) {
  436. this.answers[existingIndex] = answer;
  437. } else {
  438. this.answers.push(answer);
  439. }
  440. this.playAiSpeaking();
  441. setTimeout(() => {
  442. this.checkAnswer();
  443. this.saveAnswer();
  444. this.submitCurrentAnswer().then(() => {
  445. setTimeout(() => {
  446. this.goToNextQuestion();
  447. }, 800);
  448. }).catch((error) => {
  449. console.error("提交答案失败:", error);
  450. setTimeout(() => {
  451. this.goToNextQuestion();
  452. }, 1e3);
  453. });
  454. }, 500);
  455. }
  456. },
  457. checkAnswer() {
  458. if (this.showResult)
  459. return;
  460. clearInterval(this.timerInterval);
  461. if (!this.currentQuestion) {
  462. console.error("当前问题不存在");
  463. return;
  464. }
  465. if (this.currentQuestion.questionType === 0) {
  466. this.isAnswerCorrect = true;
  467. } else if (this.currentQuestion.questionType === 2) {
  468. const sortedSelected = [...this.selectedOptions].sort();
  469. const sortedCorrect = [...this.currentQuestion.correctAnswers].sort();
  470. if (sortedSelected.length !== sortedCorrect.length) {
  471. this.isAnswerCorrect = false;
  472. } else {
  473. this.isAnswerCorrect = sortedSelected.every((value, index) => value === sortedCorrect[index]);
  474. }
  475. } else {
  476. this.isAnswerCorrect = this.selectedOption === this.currentQuestion.correctAnswer;
  477. }
  478. this.showResult = true;
  479. this.saveAnswer();
  480. if (this.currentQuestionIndex === this.questions.length - 1) {
  481. this.$nextTick(() => {
  482. this.$forceUpdate();
  483. });
  484. }
  485. },
  486. // 修改 nextQuestion 方法,只处理多选题
  487. nextQuestion() {
  488. if (this.currentQuestion.questionType !== 2)
  489. return;
  490. this.checkAnswer();
  491. this.saveAnswer();
  492. this.submitCurrentAnswer().then(() => {
  493. setTimeout(() => {
  494. if (this.currentQuestionIndex >= this.questions.length - 1) {
  495. if (this.currentGroupIndex < this.questionGroups.length - 1) {
  496. this.showContinueButton = true;
  497. setTimeout(() => {
  498. this.scrollTop = 0;
  499. this.$nextTick(() => {
  500. setTimeout(() => {
  501. this.scrollTop = 1e4;
  502. }, 100);
  503. });
  504. }, 200);
  505. } else {
  506. common_vendor.index.navigateTo({
  507. url: "/pages/posture-guide/posture-guide"
  508. });
  509. }
  510. } else {
  511. this.goToNextQuestion();
  512. }
  513. }, 500);
  514. }).catch((error) => {
  515. console.error("提交答案失败:", error);
  516. setTimeout(() => {
  517. if (this.currentQuestionIndex >= this.questions.length - 1) {
  518. if (this.currentGroupIndex < this.questionGroups.length - 1) {
  519. this.showContinueButton = true;
  520. setTimeout(() => {
  521. this.scrollTop = 1e4;
  522. }, 200);
  523. } else {
  524. common_vendor.index.navigateTo({
  525. url: "/pages/posture-guide/posture-guide"
  526. });
  527. }
  528. } else {
  529. this.goToNextQuestion();
  530. }
  531. }, 1e3);
  532. });
  533. },
  534. toggleSettings() {
  535. common_vendor.index.showToast({
  536. title: "设置功能开发中",
  537. icon: "none"
  538. });
  539. },
  540. back(target) {
  541. if (this.timerInterval) {
  542. clearInterval(this.timerInterval);
  543. }
  544. if (target) {
  545. common_vendor.index.navigateTo({
  546. url: target
  547. });
  548. return;
  549. }
  550. },
  551. error(e) {
  552. console.error(e.detail);
  553. common_vendor.index.showToast({
  554. title: "相机启动失败,请检查权限设置",
  555. icon: "none"
  556. });
  557. },
  558. playAiSpeaking() {
  559. if (this.useVideo && this.aiVideoContext) {
  560. this.aiVideoContext.play();
  561. }
  562. if (this.digitalHumanUrl) {
  563. const speakText = this.currentQuestion ? this.currentQuestion.text : "";
  564. this.interactWithDigitalHuman(speakText);
  565. }
  566. },
  567. pauseAiSpeaking() {
  568. if (this.useVideo && this.aiVideoContext) {
  569. this.aiVideoContext.pause();
  570. }
  571. },
  572. // 修改 restartTest 方法,添加可选的跳转目标
  573. restartTest(target) {
  574. this.currentQuestionIndex = 0;
  575. this.score = 0;
  576. this.showEndModal = false;
  577. this.showResult = false;
  578. this.selectedOption = null;
  579. this.selectedOptions = [];
  580. this.resetTimer();
  581. if (target) {
  582. common_vendor.index.navigateTo({
  583. url: target
  584. });
  585. }
  586. },
  587. // 在methods中添加测试方法
  588. testEndScreen() {
  589. this.interviewCompleted = true;
  590. this.showEndModal = false;
  591. },
  592. // 初始化数字人
  593. initDigitalHuman() {
  594. this.digitalHumanUrl = "";
  595. },
  596. // 与数字人交互的方法
  597. interactWithDigitalHuman(message) {
  598. const webview = this.$mp.page.$getAppWebview().children()[0];
  599. if (webview) {
  600. webview.evalJS(`receiveMessage('${message}')`);
  601. }
  602. },
  603. // 添加 navigateToInterview 方法
  604. navigateToInterview() {
  605. this.showEndModal = false;
  606. common_vendor.index.navigateTo({
  607. url: "/pages/interview/interview",
  608. success: () => {
  609. console.log("成功跳转到interview页面");
  610. },
  611. fail: (err) => {
  612. console.error("跳转失败:", err);
  613. common_vendor.index.showToast({
  614. title: "跳转失败,请手动返回首页",
  615. icon: "none"
  616. });
  617. }
  618. });
  619. },
  620. // 添加 handleCameraError 方法
  621. handleCameraError(e) {
  622. console.error("相机错误:", e.detail);
  623. common_vendor.index.showToast({
  624. title: "相机启动失败,请检查权限设置",
  625. icon: "none"
  626. });
  627. },
  628. // 修改 isOptionSelected 方法
  629. isOptionSelected(question, qIndex, optionIndex) {
  630. const answer = this.answers.find((a) => a.questionId === question.id);
  631. if (qIndex === this.currentQuestionIndex) {
  632. if (question.questionType === 1 || question.questionType === 3 || question.questionType === 4) {
  633. return this.selectedOption === optionIndex;
  634. } else if (question.questionType === 2) {
  635. return this.selectedOptions.includes(optionIndex);
  636. }
  637. } else if (answer) {
  638. if (question.questionType === 1 || question.questionType === 3 || question.questionType === 4) {
  639. return answer.answer === optionIndex;
  640. } else if (question.questionType === 2 && Array.isArray(answer.answer)) {
  641. return answer.answer.includes(optionIndex);
  642. }
  643. }
  644. return false;
  645. },
  646. // 修改 handleOptionClick 方法
  647. handleOptionClick(qIndex, index) {
  648. if (qIndex < this.currentQuestionIndex - 1) {
  649. common_vendor.index.showToast({
  650. title: "该题目已完成,无法修改",
  651. icon: "none",
  652. duration: 2e3
  653. });
  654. return;
  655. }
  656. if (qIndex === this.questions.length - 1 && this.showContinueButton) {
  657. common_vendor.index.showToast({
  658. title: "该题目已完成,请继续下一部分",
  659. icon: "none",
  660. duration: 2e3
  661. });
  662. return;
  663. }
  664. if (qIndex > this.currentQuestionIndex) {
  665. const firstUnansweredIndex = this.currentQuestionIndex;
  666. common_vendor.index.showToast({
  667. title: `请先完成第 ${firstUnansweredIndex + 1} 题`,
  668. icon: "none",
  669. duration: 2e3
  670. });
  671. this.currentScrollId = "question-" + firstUnansweredIndex;
  672. return;
  673. }
  674. if (this._isClicking) {
  675. return;
  676. }
  677. this._isClicking = true;
  678. if (this._clickTimer) {
  679. clearTimeout(this._clickTimer);
  680. }
  681. try {
  682. if (qIndex === this.currentQuestionIndex - 1) {
  683. this.currentQuestionIndex = qIndex;
  684. this.currentScrollId = "question-" + qIndex;
  685. this.showResult = false;
  686. this.selectedOption = null;
  687. this.selectedOptions = [];
  688. }
  689. this.selectOption(index);
  690. this._clickTimer = setTimeout(() => {
  691. this._isClicking = false;
  692. }, 1e3);
  693. } catch (error) {
  694. console.error("处理选项点击失败:", error);
  695. common_vendor.index.showToast({
  696. title: "操作失败,请重试",
  697. icon: "none"
  698. });
  699. this._isClicking = false;
  700. }
  701. },
  702. // 处理题目分组
  703. processQuestionGroups(questions) {
  704. const group1 = [];
  705. const group2 = [];
  706. questions.forEach((q) => {
  707. if (q.category === 5) {
  708. group1.push(q);
  709. } else {
  710. group2.push(q);
  711. }
  712. });
  713. this.questionGroups = [group2, group1].filter((group) => group.length > 0);
  714. this.questions = this.questionGroups[0] || [];
  715. },
  716. // 处理继续按钮点击
  717. handleContinue() {
  718. if (this.currentGroupIndex < this.questionGroups.length - 1) {
  719. if (this.currentGroupIndex === 0) {
  720. this.showPromptModal = true;
  721. } else {
  722. this.proceedToNextGroup();
  723. }
  724. } else {
  725. if (this.positionConfig.enable_posture_check) {
  726. common_vendor.index.navigateTo({
  727. url: "/pages/posture-guide/posture-guide"
  728. });
  729. } else {
  730. common_vendor.index.navigateTo({
  731. url: "/pages/interview-question/interview-question"
  732. });
  733. }
  734. }
  735. },
  736. // 添加新的方法处理弹窗确认
  737. handlePromptConfirm() {
  738. this.showPromptModal = false;
  739. this.proceedToNextGroup();
  740. },
  741. // 添加新的方法处理组切换逻辑
  742. proceedToNextGroup() {
  743. this.currentGroupIndex++;
  744. this.questions = this.questionGroups[this.currentGroupIndex];
  745. this.currentQuestionIndex = 0;
  746. this.showContinueButton = false;
  747. this.currentScrollId = "question-0";
  748. this.scrollTop = 0;
  749. this.selectedOption = null;
  750. this.selectedOptions = [];
  751. this.showResult = false;
  752. },
  753. // 保存当前题目的答案
  754. saveAnswer() {
  755. let answer;
  756. if (this.currentQuestion.questionType === 0) {
  757. answer = {
  758. questionId: this.currentQuestion.id,
  759. questionType: this.currentQuestion.questionType,
  760. answer: this.openQuestionAnswer,
  761. answerDuration: this.getAnswerDuration()
  762. // 添加答题时长
  763. };
  764. } else if (this.currentQuestion.questionType === 1 || this.currentQuestion.questionType === 3 || this.currentQuestion.questionType === 4) {
  765. answer = {
  766. questionId: this.currentQuestion.id,
  767. questionType: this.currentQuestion.questionType,
  768. answer: this.selectedOption,
  769. answerDuration: this.getAnswerDuration()
  770. // 添加答题时长
  771. };
  772. } else {
  773. answer = {
  774. questionId: this.currentQuestion.id,
  775. questionType: this.currentQuestion.questionType,
  776. answer: this.selectedOptions,
  777. answerDuration: this.getAnswerDuration()
  778. // 添加答题时长
  779. };
  780. }
  781. const existingIndex = this.answers.findIndex((a) => a.questionId === answer.questionId);
  782. if (existingIndex > -1) {
  783. this.answers[existingIndex] = answer;
  784. } else {
  785. this.answers.push(answer);
  786. }
  787. this.currentAnswer = answer;
  788. console.log("已保存答案:", this.answers);
  789. },
  790. // 获取答题时长(秒)
  791. getAnswerDuration() {
  792. const remainingTimeArr = this.remainingTime.split(":");
  793. const remainingSeconds = parseInt(remainingTimeArr[0]) * 60 + parseInt(remainingTimeArr[1]);
  794. return 30 - remainingSeconds;
  795. },
  796. // 提交当前答案
  797. async submitCurrentAnswer() {
  798. if (!this.currentAnswer || this.isSubmitting)
  799. return;
  800. try {
  801. this.isSubmitting = true;
  802. let answerContent = "";
  803. let answerOptions = [];
  804. if (this.currentAnswer.questionType === 0) {
  805. answerContent = this.currentAnswer.answer;
  806. answerOptions = [];
  807. } else if (this.currentAnswer.questionType === 1 || this.currentAnswer.questionType === 3 || this.currentAnswer.questionType === 4) {
  808. const selectedIndex = this.currentAnswer.answer;
  809. const selectedOption = this.currentQuestion.options[selectedIndex];
  810. console.log("selectedOption", selectedOption);
  811. const optionId = selectedOption.id ? selectedOption.id : selectedIndex;
  812. answerContent = optionId.toString();
  813. answerOptions = [optionId];
  814. } else if (this.currentAnswer.questionType === 2) {
  815. const selectedIndices = this.currentAnswer.answer;
  816. const selectedOptionIds = selectedIndices.map((index) => {
  817. const option = this.currentQuestion.options[index];
  818. return option.id ? option.id : index;
  819. });
  820. answerOptions = selectedOptionIds;
  821. }
  822. const submitData = {
  823. position_id: JSON.parse(common_vendor.index.getStorageSync("selectedJob")).id,
  824. applicant_id: JSON.parse(common_vendor.index.getStorageSync("userInfo")).id,
  825. job_position_question_id: this.currentAnswer.questionId,
  826. // answer_content: answerContent,
  827. answer_options: answerOptions,
  828. answer_duration: this.currentAnswer.answerDuration || 0,
  829. tenant_id: JSON.parse(common_vendor.index.getStorageSync("userInfo")).tenant_id || 1
  830. };
  831. console.log("提交数据:", submitData);
  832. const res2 = await this.$http.post(`${common_config.apiBaseUrl}/api/job/submit_answer`, submitData);
  833. console.log("提交答案响应:", res2);
  834. return res2;
  835. } catch (error) {
  836. console.error("提交答案失败:", error);
  837. common_vendor.index.showToast({
  838. title: "提交答案失败,请重试",
  839. icon: "none"
  840. });
  841. throw error;
  842. } finally {
  843. common_vendor.index.hideLoading();
  844. this.isSubmitting = false;
  845. }
  846. },
  847. // 修改 goToNextQuestion 方法
  848. async goToNextQuestion() {
  849. if (this.currentQuestionIndex >= this.questions.length - 1) {
  850. this.showContinueButton = true;
  851. setTimeout(() => {
  852. this.scrollTop = 0;
  853. this.$nextTick(() => {
  854. setTimeout(() => {
  855. this.scrollTop = 1e4;
  856. }, 100);
  857. });
  858. }, 200);
  859. return;
  860. }
  861. this.currentQuestionIndex++;
  862. this.currentScrollId = "question-" + this.currentQuestionIndex;
  863. this.showResult = false;
  864. this.selectedOption = null;
  865. this.selectedOptions = [];
  866. this.openQuestionAnswer = "";
  867. if (this.questions[this.currentQuestionIndex]) {
  868. this.progressWidth = (this.currentQuestionIndex + 1) / this.questions.length * 100;
  869. this.resetTimer();
  870. this.playAiSpeaking();
  871. setTimeout(() => {
  872. this.pauseAiSpeaking();
  873. }, 2e3);
  874. }
  875. }
  876. },
  877. // 添加生命周期钩子,确保在组件销毁时清除计时器
  878. beforeDestroy() {
  879. if (this.timerInterval) {
  880. clearInterval(this.timerInterval);
  881. }
  882. }
  883. };
  884. if (!Array) {
  885. const _component_uni_load_more = common_vendor.resolveComponent("uni-load-more");
  886. _component_uni_load_more();
  887. }
  888. function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  889. return common_vendor.e({
  890. a: !$data.digitalHumanUrl
  891. }, !$data.digitalHumanUrl ? {
  892. b: $data.mode,
  893. c: common_vendor.o((...args) => $options.handleCameraError && $options.handleCameraError(...args))
  894. } : $data.digitalHumanUrl ? {
  895. e: $data.digitalHumanUrl
  896. } : {
  897. f: common_assets._imports_0
  898. }, {
  899. d: $data.digitalHumanUrl,
  900. g: $data.showPageWarning ? 1 : "",
  901. h: common_vendor.f($data.questions, (question, qIndex, i0) => {
  902. return common_vendor.e({
  903. a: common_vendor.t(qIndex + 1),
  904. b: common_vendor.t(question.questionTypeName),
  905. c: common_vendor.t(question.text),
  906. d: question.is_required_correct
  907. }, question.is_required_correct ? {} : {}, {
  908. e: question.questionType === 3 && question.imageUrl
  909. }, question.questionType === 3 && question.imageUrl ? {
  910. f: question.imageUrl
  911. } : {}, {
  912. g: question.questionType !== 0
  913. }, question.questionType !== 0 ? {
  914. h: common_vendor.f(question.options, (option, index, i1) => {
  915. return {
  916. a: common_vendor.t(option.option_text || (typeof option === "string" ? option : JSON.stringify(option))),
  917. b: index,
  918. c: $options.isOptionSelected(question, qIndex, index) ? 1 : "",
  919. d: common_vendor.o(($event) => $options.handleOptionClick(qIndex, index), index)
  920. };
  921. }),
  922. i: qIndex !== $data.currentQuestionIndex ? 1 : ""
  923. } : {}, {
  924. j: qIndex === $data.currentQuestionIndex && question.questionType === 2
  925. }, qIndex === $data.currentQuestionIndex && question.questionType === 2 ? {
  926. k: common_vendor.t("下一题"),
  927. l: common_vendor.o(($event) => $options.nextQuestion(), question.id),
  928. m: $data.selectedOptions.length === 0
  929. } : {}, {
  930. n: question.id,
  931. o: "question-" + qIndex
  932. });
  933. }),
  934. i: common_vendor.t($data.questions.length),
  935. j: $data.showContinueButton
  936. }, $data.showContinueButton ? {
  937. k: common_vendor.t($data.currentGroupIndex < $data.questionGroups.length - 1 ? "继续下一部分" : "完成测试"),
  938. l: common_vendor.o((...args) => $options.handleContinue && $options.handleContinue(...args))
  939. } : {}, {
  940. m: $data.currentScrollId,
  941. n: $data.scrollTop,
  942. o: $data.showEndModal
  943. }, $data.showEndModal ? {
  944. p: common_vendor.o((...args) => $options.navigateToInterview && $options.navigateToInterview(...args))
  945. } : {}, {
  946. q: $data.interviewCompleted
  947. }, $data.interviewCompleted ? {
  948. r: common_assets._imports_0,
  949. s: common_vendor.o((...args) => $options.back && $options.back(...args))
  950. } : {}, {
  951. t: $data.loading
  952. }, $data.loading ? {
  953. v: common_vendor.p({
  954. status: "loading",
  955. contentText: {
  956. contentdown: "加载中..."
  957. }
  958. })
  959. } : {}, {
  960. w: !$data.loading && $data.loadError
  961. }, !$data.loading && $data.loadError ? {
  962. x: common_vendor.o((...args) => $options.fetchInterviewData && $options.fetchInterviewData(...args))
  963. } : {}, {
  964. y: $data.showPromptModal
  965. }, $data.showPromptModal ? {
  966. z: common_vendor.o((...args) => $options.handlePromptConfirm && $options.handlePromptConfirm(...args))
  967. } : {});
  968. }
  969. const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
  970. wx.createPage(MiniProgramPage);