camera.js 41 KB

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