yangg 3 minggu lalu
induk
melakukan
e7bac94eb8

+ 4 - 1
common/config.js

@@ -14,4 +14,7 @@ export const appVersion = '1.0.0';
 //   chunkSize: 1024 * 1024,  // 分块上传大小(1MB)
 //   timeout: 60000,          // 上传超时时间(毫秒)
 //   validateChecksum: true   // 是否验证校验和
-// }; 
+// };
+
+// WebSocket URL for person detection (do not hardcode elsewhere)
+export const personDetectionWsUrl = `ws://192.168.100.101:8083`; 

+ 238 - 2
pages/camera/camera.vue

@@ -2,7 +2,7 @@
 	<view class="camera-container">
 		<!-- 顶部相机区域 - 固定位置 -->
 		<view class="fixed-camera-container">
-			<view class="user-avatar">
+			<view class="user-avatar" :class="{ 'camera-warning': showPageWarning }">
 				<camera v-if="!digitalHumanUrl" device-position="front" flash="auto" class="camera" 
 						:mode="mode" @error="handleCameraError"></camera>
 				<web-view v-else-if="digitalHumanUrl" :src="digitalHumanUrl" class="digital-human-webview"></web-view>
@@ -131,7 +131,7 @@
 
 <script>
 	import { getInterviewList, getInterviewDetail,getQuestions } from '@/api/user.js';
-	import { apiBaseUrl } from '@/common/config.js';
+	import { apiBaseUrl,personDetectionWsUrl } from '@/common/config.js';
 	export default {
 		data() {
 			return {
@@ -175,6 +175,10 @@
 				_clickTimer: null,
 				_isClicking: false,
 				showPromptModal: false, // 添加新的数据属性
+				personDetectionSocket: null, // WebSocket对象
+				personDetectionInterval: null, // 定时器对象
+				showCameraWarning: false, // 添加新的数据属性
+				showPageWarning: false, // 添加新的数据属性
 			}
 		},
 		computed: {
@@ -204,7 +208,220 @@
 			// 初始化数字人
 			this.initDigitalHuman();
 		},
+		mounted() {
+			 // 添加截屏监听
+			 uni.onUserCaptureScreen(() => {
+			console.log('User captured screen');
+			this.screenCaptureCount++;
+			
+			if (this.screenCaptureCount === 1) {
+				uni.showModal({
+				title: 'Warning',
+				content: 'Screen capture detected. If you capture the screen again, your interview will be invalid.',
+				showCancel: false,
+				confirmText: 'OK'
+				});
+			} else if (this.screenCaptureCount >= 2) {
+				uni.showModal({
+				title: 'Interview Invalid',
+				content: 'Your interview has been invalidated due to multiple screen captures. Please contact the recruiter for assistance.',
+				showCancel: false,
+				confirmText: 'OK',
+				success: () => {
+					this.invalidateInterview();
+				}
+				});
+			}
+			});
+		},
+		beforeDestroy() {
+    // 移除截屏监听
+    this.cleanupPersonDetectionWebSocket();
+  },
 		methods: {
+			initPersonDetectionWebSocket() {
+      if (this.personDetectionSocket) {
+        this.cleanupPersonDetectionWebSocket();
+      }
+
+      try {
+        this.personDetectionSocket = uni.connectSocket({
+          url: `${personDetectionWsUrl}/ws/interview-room/room_${uni.getStorageSync('appId')}/${uni.getStorageSync('appId')}/`,
+          success: () => {
+            console.log('WebSocket connection initiated');
+          },
+          fail: (error) => {
+            console.error('WebSocket connection failed:', error);
+          }
+        });
+
+        this.personDetectionSocket.onOpen(() => {
+          console.log('WebSocket connection opened');
+          this.startPersonDetectionInterval();
+        });
+
+        this.personDetectionSocket.onError((error) => {
+          console.error('WebSocket error:', error);
+          this.cleanupPersonDetectionWebSocket();
+        });
+
+        this.personDetectionSocket.onClose(() => {
+          console.log('WebSocket connection closed');
+          this.cleanupPersonDetectionWebSocket();
+        });
+
+        this.personDetectionSocket.onMessage((res) => {
+          try {
+            const data = JSON.parse(res.data);
+            console.log(data);
+            if (data.type === 'person_detection_result') {
+              this.handlePersonDetectionResult(data);
+            }
+          } catch (error) {
+            console.error('Error parsing WebSocket message:', error);
+          }
+        });
+      } catch (error) {
+        console.error('Error initializing WebSocket:', error);
+        this.cleanupPersonDetectionWebSocket();
+      }
+    },
+
+    startPersonDetectionInterval() {
+      if (this.personDetectionInterval) {
+        clearInterval(this.personDetectionInterval);
+      }
+
+      this.personDetectionInterval = setInterval(() => {
+        if (this.personDetectionSocket && this.cameraContext) {
+          this.cameraContext.takePhoto({
+            quality: 'low',
+            success: (res) => {
+              const tempFilePath = res.tempImagePath;
+              uni.getFileSystemManager().readFile({
+                filePath: tempFilePath,
+                encoding: 'base64',
+                success: (res) => {
+                  const base64Image = res.data;
+                  this.personDetectionSocket.send({
+                    data: JSON.stringify({
+                      type: 'person_detection',
+                      image_data: base64Image
+                    })
+                  });
+                },
+                fail: (error) => {
+                  console.error('Error reading image file:', error);
+                }
+              });
+            },
+            fail: (error) => {
+              console.error('Error taking photo:', error);
+            }
+          });
+        }
+      }, 3000);
+    },
+
+    cleanupPersonDetectionWebSocket() {
+      if (this.personDetectionInterval) {
+        clearInterval(this.personDetectionInterval);
+        this.personDetectionInterval = null;
+      }
+
+      if (this.personDetectionSocket) {
+        try {
+          this.personDetectionSocket.close();
+        } catch (error) {
+          console.error('Error closing WebSocket:', error);
+        }
+        this.personDetectionSocket = null;
+      }
+    },
+
+    handlePersonDetectionResult(data) {
+      console.log(data.data.detection.has_person);
+      console.log(data.data.identity.status);//identity_verified
+      
+      // 首先检查是否有人
+     /*  if (!data.data.detection.has_person) {
+        this.showPageWarning = true;
+        uni.showToast({
+          title: '请勿离开摄像头',
+          icon: 'none',
+          duration: 3000
+        });
+        // 添加震动反馈
+        uni.vibrateShort({
+          success: function () {
+            console.log('Vibration successful');
+          },
+          fail: function (err) {
+            console.error('Vibration failed:', err);
+          }
+        });
+        setTimeout(() => {
+          this.showPageWarning = false;
+        }, 3000);
+      }
+      // 然后检查身份验证状态
+      else */ if (data.data.identity.status !== "identity_verified") {
+        this.showPageWarning = true;
+        uni.showToast({
+          title: data.data.identity.message,
+          icon: 'none',
+          duration: 3000
+        });
+        uni.vibrateLong({
+          success: function () {
+            console.log('Vibration successful');
+          },
+          fail: function (err) {
+            console.error('Vibration failed:', err);
+          }
+        });
+        setTimeout(() => {
+          this.showPageWarning = false;
+        }, 3000);
+      }
+    },
+			// 添加作废面试的方法
+			invalidateInterview() {
+				// 停止录制(如果正在录制)
+				if (this.isRecording) {
+					this.stopRecordingAnswer();
+				}
+				
+				// 清除所有计时器
+				if (this.recordingTimer) {
+					clearInterval(this.recordingTimer);
+				}
+				
+				// 重置状态
+				this.isRecording = false;
+				this.showStopRecordingButton = false;
+				this.showStartRecordingButton = false;
+				
+				// 显示作废提示
+				// uni.showToast({
+				//   title: 'Interview invalidated',
+				//   icon: 'none',
+				//   duration: 2000
+				// });
+				
+				// 延迟后返回上一页
+				setTimeout(() => {
+					uni.switchTab({
+					url: '/pages/index/index'
+				});
+				}, 500);
+
+				const systemInfo = uni.getSystemInfoSync();
+				const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith('mp-');
+				if (isMiniProgram) {
+				this.initPersonDetectionWebSocket();
+				}
+			},
 			// 获取面试列表
 			async fetchInterviewList() {
 				try {
@@ -1678,6 +1895,25 @@
 		justify-content: flex-start;
 		border-bottom: 1px solid #eee;
 	}
+	.camera-warning {
+	animation: camera-warning-flash 1s ease-in-out infinite;
+	border: 2px solid #ff0000;
+	}
+
+	@keyframes camera-warning-flash {
+	0% { 
+		border-color: #ff0000;
+		box-shadow: 0 0 15px rgba(255, 0, 0, 0.8);
+	}
+	50% { 
+		border-color: rgba(255, 0, 0, 0.3);
+		box-shadow: 0 0 5px rgba(255, 0, 0, 0.4);
+	}
+	100% { 
+		border-color: #ff0000;
+		box-shadow: 0 0 15px rgba(255, 0, 0, 0.8);
+	}
+	}
 	
 	.user-avatar {
 		width: 110px;

+ 210 - 14
pages/identity-verify/identity-verify.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="identity-verify-container">
+  <div class="identity-verify-container" :class="{ 'page-warning': showPageWarning }">
     <div class="digital-human-container">
       <!-- 添加题号显示 -->
      <!--  <div class="question-counter" v-if="currentVideoIndex > 0">
@@ -74,7 +74,7 @@
       </div>
       
       <!-- 用户摄像头视频显示区域 -->
-      <div class="user-camera-container">
+      <div class="user-camera-container" :class="{ 'camera-warning': showPageWarning }">
         <!-- 在小程序环境中使用camera组件 -->
         <camera v-if="useMiniProgramCameraComponent" 
                 device-position="front" 
@@ -207,7 +207,7 @@
 </template>
 
 <script>
-import { apiBaseUrl } from '@/common/config.js';
+import { apiBaseUrl, personDetectionWsUrl } from '@/common/config.js';
 export default {
   name: 'IdentityVerify',
   data() {
@@ -297,6 +297,10 @@ export default {
       progressBgColor: 'rgba(0, 0, 0,0.3)', // 进度条背景色
       parentQuestion: '', // 添加父问题存储
       screenCaptureCount: 0, // 添加截屏次数记录
+      personDetectionSocket: null, // WebSocket对象
+      personDetectionInterval: null, // 定时器对象
+      showCameraWarning: false, // 添加新的数据属性
+      showPageWarning: false, // 添加新的数据属性
     }
   },
   mounted() {
@@ -322,31 +326,35 @@ export default {
       this.screenCaptureCount++;
       
       if (this.screenCaptureCount === 1) {
-        // 第一次截屏,显示警告
         uni.showModal({
-          title: '警告',
-          content: '检测到屏幕截图。如果你再次捕捉屏幕,你的面试结果将无效。',
+          title: 'Warning',
+          content: 'Screen capture detected. If you capture the screen again, your interview will be invalid.',
           showCancel: false,
           confirmText: 'OK'
         });
       } else if (this.screenCaptureCount >= 2) {
-        // 第二次及以上截屏,作废成绩
         uni.showModal({
-          title: '面试作废',
-          content: '由于您在面试过程中多次截屏,本次面试作废,如有疑问请联系企业招聘人员',
+          title: 'Interview Invalid',
+          content: 'Your interview has been invalidated due to multiple screen captures. Please contact the recruiter for assistance.',
           showCancel: false,
           confirmText: 'OK',
           success: () => {
-            // 可以在这里添加其他处理逻辑,比如记录日志或上报服务器
             this.invalidateInterview();
           }
         });
       }
     });
+
+    const systemInfo = uni.getSystemInfoSync();
+    const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith('mp-');
+    if (isMiniProgram) {
+      this.initPersonDetectionWebSocket();
+    }
   },
   beforeDestroy() {
     // 移除截屏监听
     uni.offUserCaptureScreen();
+    this.cleanupPersonDetectionWebSocket();
   },
   methods: {
     // 初始化相机
@@ -3358,6 +3366,153 @@ export default {
       });
       }, 500);
     },
+
+    initPersonDetectionWebSocket() {
+      if (this.personDetectionSocket) {
+        this.cleanupPersonDetectionWebSocket();
+      }
+
+      try {
+        this.personDetectionSocket = uni.connectSocket({
+          url: `${personDetectionWsUrl}/ws/interview-room/room_${uni.getStorageSync('appId')}/${uni.getStorageSync('appId')}/`,
+          success: () => {
+            console.log('WebSocket connection initiated');
+          },
+          fail: (error) => {
+            console.error('WebSocket connection failed:', error);
+          }
+        });
+
+        this.personDetectionSocket.onOpen(() => {
+          console.log('WebSocket connection opened');
+          this.startPersonDetectionInterval();
+        });
+
+        this.personDetectionSocket.onError((error) => {
+          console.error('WebSocket error:', error);
+          this.cleanupPersonDetectionWebSocket();
+        });
+
+        this.personDetectionSocket.onClose(() => {
+          console.log('WebSocket connection closed');
+          this.cleanupPersonDetectionWebSocket();
+        });
+
+        this.personDetectionSocket.onMessage((res) => {
+          try {
+            const data = JSON.parse(res.data);
+            console.log(data);
+            if (data.type === 'person_detection_result') {
+              this.handlePersonDetectionResult(data);
+            }
+          } catch (error) {
+            console.error('Error parsing WebSocket message:', error);
+          }
+        });
+      } catch (error) {
+        console.error('Error initializing WebSocket:', error);
+        this.cleanupPersonDetectionWebSocket();
+      }
+    },
+
+    startPersonDetectionInterval() {
+      if (this.personDetectionInterval) {
+        clearInterval(this.personDetectionInterval);
+      }
+
+      this.personDetectionInterval = setInterval(() => {
+        if (this.personDetectionSocket && this.cameraContext) {
+          this.cameraContext.takePhoto({
+            quality: 'low',
+            success: (res) => {
+              const tempFilePath = res.tempImagePath;
+              uni.getFileSystemManager().readFile({
+                filePath: tempFilePath,
+                encoding: 'base64',
+                success: (res) => {
+                  const base64Image = res.data;
+                  this.personDetectionSocket.send({
+                    data: JSON.stringify({
+                      type: 'person_detection',
+                      image_data: base64Image
+                    })
+                  });
+                },
+                fail: (error) => {
+                  console.error('Error reading image file:', error);
+                }
+              });
+            },
+            fail: (error) => {
+              console.error('Error taking photo:', error);
+            }
+          });
+        }
+      }, 3000);
+    },
+
+    cleanupPersonDetectionWebSocket() {
+      if (this.personDetectionInterval) {
+        clearInterval(this.personDetectionInterval);
+        this.personDetectionInterval = null;
+      }
+
+      if (this.personDetectionSocket) {
+        try {
+          this.personDetectionSocket.close();
+        } catch (error) {
+          console.error('Error closing WebSocket:', error);
+        }
+        this.personDetectionSocket = null;
+      }
+    },
+
+    handlePersonDetectionResult(data) {
+      console.log(data.data.detection.has_person);
+      console.log(data.data.identity.status);//identity_verified
+      
+      /* // 首先检查是否有人
+      if (!data.data.detection.has_person) {
+        this.showPageWarning = true;
+        uni.showToast({
+          title: '请勿离开摄像头',
+          icon: 'none',
+          duration: 3000
+        });
+        // 添加震动反馈
+        uni.vibrateLong({
+          success: function () {
+            console.log('Vibration successful');
+          },
+          fail: function (err) {
+            console.error('Vibration failed:', err);
+          }
+        });
+        setTimeout(() => {
+          this.showPageWarning = false;
+        }, 3000);
+      }
+      // 然后检查身份验证状态
+      else */ if (data.data.identity.status !== "identity_verified") {
+        this.showPageWarning = true;
+        uni.showToast({
+          title: data.data.identity.message,
+          icon: 'none',
+          duration: 3000
+        });
+        uni.vibrateLong({
+          success: function () {
+            console.log('Vibration successful');
+          },
+          fail: function (err) {
+            console.error('Vibration failed:', err);
+          }
+        });
+        setTimeout(() => {
+          this.showPageWarning = false;
+        }, 3000);
+      }
+    },
   },
   computed: {
     // 计算进度比例
@@ -3407,6 +3562,27 @@ export default {
   display: flex;
   flex-direction: column;
   background-color: #f5f5f5;
+  transition: all 0.3s ease;
+  position: relative;
+}
+
+.page-warning {
+  animation: page-warning-flash 1s ease-in-out infinite;
+}
+
+@keyframes page-warning-flash {
+  0% { 
+    background-color: rgba(255, 0, 0, 0.2);
+    box-shadow: 0 0 20px rgba(255, 0, 0, 0.3);
+  }
+  50% { 
+    background-color: rgba(255, 0, 0, 0.1);
+    box-shadow: 0 0 10px rgba(255, 0, 0, 0.2);
+  }
+  100% { 
+    background-color: rgba(255, 0, 0, 0.2);
+    box-shadow: 0 0 20px rgba(255, 0, 0, 0.3);
+  }
 }
 
 .digital-human-container {
@@ -3430,13 +3606,33 @@ export default {
   position: absolute;
   top: 50px;
   right: 5px;
-  width: 110px; /* 稍微增加宽度 */
-  height: 160px; /* 稍微增加高度 */
-  border-radius: 4px; /* 减小圆角 */
+  width: 110px;
+  height: 160px;
+  border-radius: 4px;
   overflow: hidden;
   box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
   z-index: 20;
- 
+  transition: all 0.3s ease;
+}
+
+.camera-warning {
+  animation: camera-warning-flash 1s ease-in-out infinite;
+  border: 4px solid #ff0000;
+}
+
+@keyframes camera-warning-flash {
+  0% { 
+    border-color: #ff0000;
+    box-shadow: 0 0 15px rgba(255, 0, 0, 0.8);
+  }
+  50% { 
+    border-color: rgba(255, 0, 0, 0.3);
+    box-shadow: 0 0 5px rgba(255, 0, 0, 0.4);
+  }
+  100% { 
+    border-color: #ff0000;
+    box-shadow: 0 0 15px rgba(255, 0, 0, 0.8);
+  }
 }
 
 /* 用户摄像头视频样式 */

+ 2 - 0
unpackage/dist/dev/mp-weixin/common/config.js

@@ -1,3 +1,5 @@
 "use strict";
 const apiBaseUrl = "http://192.168.100.101:8083";
+const personDetectionWsUrl = `ws://192.168.100.101:8083`;
 exports.apiBaseUrl = apiBaseUrl;
+exports.personDetectionWsUrl = personDetectionWsUrl;

+ 190 - 19
unpackage/dist/dev/mp-weixin/pages/camera/camera.js

@@ -64,7 +64,15 @@ const _sfc_main = {
       // 控制 scroll-view 的滚动位置
       _clickTimer: null,
       _isClicking: false,
-      showPromptModal: false
+      showPromptModal: false,
+      // 添加新的数据属性
+      personDetectionSocket: null,
+      // WebSocket对象
+      personDetectionInterval: null,
+      // 定时器对象
+      showCameraWarning: false,
+      // 添加新的数据属性
+      showPageWarning: false
       // 添加新的数据属性
     };
   },
@@ -89,7 +97,169 @@ const _sfc_main = {
     }
     this.initDigitalHuman();
   },
+  mounted() {
+    common_vendor.index.onUserCaptureScreen(() => {
+      console.log("User captured screen");
+      this.screenCaptureCount++;
+      if (this.screenCaptureCount === 1) {
+        common_vendor.index.showModal({
+          title: "Warning",
+          content: "Screen capture detected. If you capture the screen again, your interview will be invalid.",
+          showCancel: false,
+          confirmText: "OK"
+        });
+      } else if (this.screenCaptureCount >= 2) {
+        common_vendor.index.showModal({
+          title: "Interview Invalid",
+          content: "Your interview has been invalidated due to multiple screen captures. Please contact the recruiter for assistance.",
+          showCancel: false,
+          confirmText: "OK",
+          success: () => {
+            this.invalidateInterview();
+          }
+        });
+      }
+    });
+  },
+  beforeDestroy() {
+    this.cleanupPersonDetectionWebSocket();
+  },
   methods: {
+    initPersonDetectionWebSocket() {
+      if (this.personDetectionSocket) {
+        this.cleanupPersonDetectionWebSocket();
+      }
+      try {
+        this.personDetectionSocket = common_vendor.index.connectSocket({
+          url: `${common_config.personDetectionWsUrl}/ws/interview-room/room_${common_vendor.index.getStorageSync("appId")}/${common_vendor.index.getStorageSync("appId")}/`,
+          success: () => {
+            console.log("WebSocket connection initiated");
+          },
+          fail: (error) => {
+            console.error("WebSocket connection failed:", error);
+          }
+        });
+        this.personDetectionSocket.onOpen(() => {
+          console.log("WebSocket connection opened");
+          this.startPersonDetectionInterval();
+        });
+        this.personDetectionSocket.onError((error) => {
+          console.error("WebSocket error:", error);
+          this.cleanupPersonDetectionWebSocket();
+        });
+        this.personDetectionSocket.onClose(() => {
+          console.log("WebSocket connection closed");
+          this.cleanupPersonDetectionWebSocket();
+        });
+        this.personDetectionSocket.onMessage((res2) => {
+          try {
+            const data = JSON.parse(res2.data);
+            console.log(data);
+            if (data.type === "person_detection_result") {
+              this.handlePersonDetectionResult(data);
+            }
+          } catch (error) {
+            console.error("Error parsing WebSocket message:", error);
+          }
+        });
+      } catch (error) {
+        console.error("Error initializing WebSocket:", error);
+        this.cleanupPersonDetectionWebSocket();
+      }
+    },
+    startPersonDetectionInterval() {
+      if (this.personDetectionInterval) {
+        clearInterval(this.personDetectionInterval);
+      }
+      this.personDetectionInterval = setInterval(() => {
+        if (this.personDetectionSocket && this.cameraContext) {
+          this.cameraContext.takePhoto({
+            quality: "low",
+            success: (res2) => {
+              const tempFilePath = res2.tempImagePath;
+              common_vendor.index.getFileSystemManager().readFile({
+                filePath: tempFilePath,
+                encoding: "base64",
+                success: (res3) => {
+                  const base64Image = res3.data;
+                  this.personDetectionSocket.send({
+                    data: JSON.stringify({
+                      type: "person_detection",
+                      image_data: base64Image
+                    })
+                  });
+                },
+                fail: (error) => {
+                  console.error("Error reading image file:", error);
+                }
+              });
+            },
+            fail: (error) => {
+              console.error("Error taking photo:", error);
+            }
+          });
+        }
+      }, 3e3);
+    },
+    cleanupPersonDetectionWebSocket() {
+      if (this.personDetectionInterval) {
+        clearInterval(this.personDetectionInterval);
+        this.personDetectionInterval = null;
+      }
+      if (this.personDetectionSocket) {
+        try {
+          this.personDetectionSocket.close();
+        } catch (error) {
+          console.error("Error closing WebSocket:", error);
+        }
+        this.personDetectionSocket = null;
+      }
+    },
+    handlePersonDetectionResult(data) {
+      console.log(data.data.detection.has_person);
+      console.log(data.data.identity.status);
+      if (data.data.identity.status !== "identity_verified") {
+        this.showPageWarning = true;
+        common_vendor.index.showToast({
+          title: data.data.identity.message,
+          icon: "none",
+          duration: 3e3
+        });
+        common_vendor.index.vibrateLong({
+          success: function() {
+            console.log("Vibration successful");
+          },
+          fail: function(err) {
+            console.error("Vibration failed:", err);
+          }
+        });
+        setTimeout(() => {
+          this.showPageWarning = false;
+        }, 3e3);
+      }
+    },
+    // 添加作废面试的方法
+    invalidateInterview() {
+      if (this.isRecording) {
+        this.stopRecordingAnswer();
+      }
+      if (this.recordingTimer) {
+        clearInterval(this.recordingTimer);
+      }
+      this.isRecording = false;
+      this.showStopRecordingButton = false;
+      this.showStartRecordingButton = false;
+      setTimeout(() => {
+        common_vendor.index.switchTab({
+          url: "/pages/index/index"
+        });
+      }, 500);
+      const systemInfo = common_vendor.index.getSystemInfoSync();
+      const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith("mp-");
+      if (isMiniProgram) {
+        this.initPersonDetectionWebSocket();
+      }
+    },
     // 获取面试列表
     async fetchInterviewList() {
       try {
@@ -712,7 +882,8 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
     f: common_assets._imports_0
   }, {
     d: $data.digitalHumanUrl,
-    g: common_vendor.f($data.questions, (question, qIndex, i0) => {
+    g: $data.showPageWarning ? 1 : "",
+    h: common_vendor.f($data.questions, (question, qIndex, i0) => {
       return common_vendor.e({
         a: common_vendor.t(qIndex + 1),
         b: common_vendor.t(question.questionTypeName),
@@ -745,39 +916,39 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
         o: "question-" + qIndex
       });
     }),
-    h: common_vendor.t($data.questions.length),
-    i: $data.showContinueButton
+    i: common_vendor.t($data.questions.length),
+    j: $data.showContinueButton
   }, $data.showContinueButton ? {
-    j: common_vendor.t($data.currentGroupIndex < $data.questionGroups.length - 1 ? "继续下一部分" : "完成测试"),
-    k: common_vendor.o((...args) => $options.handleContinue && $options.handleContinue(...args))
+    k: common_vendor.t($data.currentGroupIndex < $data.questionGroups.length - 1 ? "继续下一部分" : "完成测试"),
+    l: common_vendor.o((...args) => $options.handleContinue && $options.handleContinue(...args))
   } : {}, {
-    l: $data.currentScrollId,
-    m: $data.scrollTop,
-    n: $data.showEndModal
+    m: $data.currentScrollId,
+    n: $data.scrollTop,
+    o: $data.showEndModal
   }, $data.showEndModal ? {
-    o: common_vendor.o((...args) => $options.navigateToInterview && $options.navigateToInterview(...args))
+    p: common_vendor.o((...args) => $options.navigateToInterview && $options.navigateToInterview(...args))
   } : {}, {
-    p: $data.interviewCompleted
+    q: $data.interviewCompleted
   }, $data.interviewCompleted ? {
-    q: common_assets._imports_0,
-    r: common_vendor.o((...args) => $options.back && $options.back(...args))
+    r: common_assets._imports_0,
+    s: common_vendor.o((...args) => $options.back && $options.back(...args))
   } : {}, {
-    s: $data.loading
+    t: $data.loading
   }, $data.loading ? {
-    t: common_vendor.p({
+    v: common_vendor.p({
       status: "loading",
       contentText: {
         contentdown: "加载中..."
       }
     })
   } : {}, {
-    v: !$data.loading && $data.loadError
+    w: !$data.loading && $data.loadError
   }, !$data.loading && $data.loadError ? {
-    w: common_vendor.o((...args) => $options.fetchInterviewData && $options.fetchInterviewData(...args))
+    x: common_vendor.o((...args) => $options.fetchInterviewData && $options.fetchInterviewData(...args))
   } : {}, {
-    x: $data.showPromptModal
+    y: $data.showPromptModal
   }, $data.showPromptModal ? {
-    y: common_vendor.o((...args) => $options.handlePromptConfirm && $options.handlePromptConfirm(...args))
+    z: common_vendor.o((...args) => $options.handlePromptConfirm && $options.handlePromptConfirm(...args))
   } : {});
 }
 const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

File diff ditekan karena terlalu besar
+ 0 - 0
unpackage/dist/dev/mp-weixin/pages/camera/camera.wxml


+ 18 - 0
unpackage/dist/dev/mp-weixin/pages/camera/camera.wxss

@@ -551,6 +551,24 @@
 		justify-content: flex-start;
 		border-bottom: 1px solid #eee;
 }
+.camera-warning {
+	animation: camera-warning-flash 1s ease-in-out infinite;
+	border: 2px solid #ff0000;
+}
+@keyframes camera-warning-flash {
+0% { 
+		border-color: #ff0000;
+		box-shadow: 0 0 15px rgba(255, 0, 0, 0.8);
+}
+50% { 
+		border-color: rgba(255, 0, 0, 0.3);
+		box-shadow: 0 0 5px rgba(255, 0, 0, 0.4);
+}
+100% { 
+		border-color: #ff0000;
+		box-shadow: 0 0 15px rgba(255, 0, 0, 0.8);
+}
+}
 .user-avatar {
 		width: 110px;
 		height: 160px;

+ 156 - 26
unpackage/dist/dev/mp-weixin/pages/identity-verify/identity-verify.js

@@ -141,8 +141,16 @@ const _sfc_main = {
       // 进度条背景色
       parentQuestion: "",
       // 添加父问题存储
-      screenCaptureCount: 0
+      screenCaptureCount: 0,
       // 添加截屏次数记录
+      personDetectionSocket: null,
+      // WebSocket对象
+      personDetectionInterval: null,
+      // 定时器对象
+      showCameraWarning: false,
+      // 添加新的数据属性
+      showPageWarning: false
+      // 添加新的数据属性
     };
   },
   mounted() {
@@ -166,15 +174,15 @@ const _sfc_main = {
       this.screenCaptureCount++;
       if (this.screenCaptureCount === 1) {
         common_vendor.index.showModal({
-          title: "警告",
-          content: "检测到屏幕截图。如果你再次捕捉屏幕,你的面试结果将无效。",
+          title: "Warning",
+          content: "Screen capture detected. If you capture the screen again, your interview will be invalid.",
           showCancel: false,
           confirmText: "OK"
         });
       } else if (this.screenCaptureCount >= 2) {
         common_vendor.index.showModal({
-          title: "面试作废",
-          content: "由于您在面试过程中多次截屏,本次面试作废,如有疑问请联系企业招聘人员",
+          title: "Interview Invalid",
+          content: "Your interview has been invalidated due to multiple screen captures. Please contact the recruiter for assistance.",
           showCancel: false,
           confirmText: "OK",
           success: () => {
@@ -183,9 +191,15 @@ const _sfc_main = {
         });
       }
     });
+    const systemInfo = common_vendor.index.getSystemInfoSync();
+    const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith("mp-");
+    if (isMiniProgram) {
+      this.initPersonDetectionWebSocket();
+    }
   },
   beforeDestroy() {
     common_vendor.index.offUserCaptureScreen();
+    this.cleanupPersonDetectionWebSocket();
   },
   methods: {
     // 初始化相机
@@ -2320,6 +2334,119 @@ const _sfc_main = {
           url: "/pages/index/index"
         });
       }, 500);
+    },
+    initPersonDetectionWebSocket() {
+      if (this.personDetectionSocket) {
+        this.cleanupPersonDetectionWebSocket();
+      }
+      try {
+        this.personDetectionSocket = common_vendor.index.connectSocket({
+          url: `${common_config.personDetectionWsUrl}/ws/interview-room/room_${common_vendor.index.getStorageSync("appId")}/${common_vendor.index.getStorageSync("appId")}/`,
+          success: () => {
+            console.log("WebSocket connection initiated");
+          },
+          fail: (error) => {
+            console.error("WebSocket connection failed:", error);
+          }
+        });
+        this.personDetectionSocket.onOpen(() => {
+          console.log("WebSocket connection opened");
+          this.startPersonDetectionInterval();
+        });
+        this.personDetectionSocket.onError((error) => {
+          console.error("WebSocket error:", error);
+          this.cleanupPersonDetectionWebSocket();
+        });
+        this.personDetectionSocket.onClose(() => {
+          console.log("WebSocket connection closed");
+          this.cleanupPersonDetectionWebSocket();
+        });
+        this.personDetectionSocket.onMessage((res) => {
+          try {
+            const data = JSON.parse(res.data);
+            console.log(data);
+            if (data.type === "person_detection_result") {
+              this.handlePersonDetectionResult(data);
+            }
+          } catch (error) {
+            console.error("Error parsing WebSocket message:", error);
+          }
+        });
+      } catch (error) {
+        console.error("Error initializing WebSocket:", error);
+        this.cleanupPersonDetectionWebSocket();
+      }
+    },
+    startPersonDetectionInterval() {
+      if (this.personDetectionInterval) {
+        clearInterval(this.personDetectionInterval);
+      }
+      this.personDetectionInterval = setInterval(() => {
+        if (this.personDetectionSocket && this.cameraContext) {
+          this.cameraContext.takePhoto({
+            quality: "low",
+            success: (res) => {
+              const tempFilePath = res.tempImagePath;
+              common_vendor.index.getFileSystemManager().readFile({
+                filePath: tempFilePath,
+                encoding: "base64",
+                success: (res2) => {
+                  const base64Image = res2.data;
+                  this.personDetectionSocket.send({
+                    data: JSON.stringify({
+                      type: "person_detection",
+                      image_data: base64Image
+                    })
+                  });
+                },
+                fail: (error) => {
+                  console.error("Error reading image file:", error);
+                }
+              });
+            },
+            fail: (error) => {
+              console.error("Error taking photo:", error);
+            }
+          });
+        }
+      }, 3e3);
+    },
+    cleanupPersonDetectionWebSocket() {
+      if (this.personDetectionInterval) {
+        clearInterval(this.personDetectionInterval);
+        this.personDetectionInterval = null;
+      }
+      if (this.personDetectionSocket) {
+        try {
+          this.personDetectionSocket.close();
+        } catch (error) {
+          console.error("Error closing WebSocket:", error);
+        }
+        this.personDetectionSocket = null;
+      }
+    },
+    handlePersonDetectionResult(data) {
+      console.log(data.data.detection.has_person);
+      console.log(data.data.identity.status);
+      if (data.data.identity.status !== "identity_verified") {
+        this.showPageWarning = true;
+        common_vendor.index.showToast({
+          title: data.data.identity.message,
+          icon: "none",
+          duration: 3e3
+        });
+        common_vendor.index.vibrateLong({
+          success: function() {
+            console.log("Vibration successful");
+          },
+          fail: function(err) {
+            console.error("Vibration failed:", err);
+          }
+        });
+        setTimeout(() => {
+          this.showPageWarning = false;
+        }, 3e3);
+      }
     }
   },
   computed: {
@@ -2383,19 +2510,20 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
   }, $data.useMiniProgramCameraComponent ? {
     r: common_vendor.o((...args) => $options.handleCameraError && $options.handleCameraError(...args))
   } : {}, {
-    s: $data.loading
+    s: $data.showPageWarning ? 1 : "",
+    t: $data.loading
   }, $data.loading ? {} : {}, {
-    t: $data.showDebugInfo
+    v: $data.showDebugInfo
   }, $data.showDebugInfo ? common_vendor.e({
-    v: $data.assistantResponse
+    w: $data.assistantResponse
   }, $data.assistantResponse ? {
-    w: common_vendor.t($data.assistantResponse)
+    x: common_vendor.t($data.assistantResponse)
   } : {}, {
-    x: $data.audioTranscript
+    y: $data.audioTranscript
   }, $data.audioTranscript ? {
-    y: common_vendor.t($data.audioTranscript)
+    z: common_vendor.t($data.audioTranscript)
   } : {}, {
-    z: common_vendor.f($data.processedResponses, (item, index, i0) => {
+    A: common_vendor.f($data.processedResponses, (item, index, i0) => {
       return common_vendor.e({
         a: item.role
       }, item.role ? {
@@ -2409,31 +2537,33 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
       });
     })
   }) : {}, {
-    A: $data.showStopRecordingButton
+    B: $data.showStopRecordingButton
   }, $data.showStopRecordingButton ? {
-    B: common_vendor.o((...args) => $options.stopRecordingAnswer && $options.stopRecordingAnswer(...args))
+    C: common_vendor.o((...args) => $options.stopRecordingAnswer && $options.stopRecordingAnswer(...args))
   } : {}, {
-    C: $data.isRecording
+    D: $data.isRecording
   }, $data.isRecording ? {
-    D: common_vendor.t($data.recordingTimeDisplay),
-    E: `conic-gradient(${$data.progressColor} ${$options.progressPercent}%, ${$data.progressBgColor} 0%)`
+    E: common_vendor.t($data.recordingTimeDisplay),
+    F: `conic-gradient(${$data.progressColor} ${$options.progressPercent}%, ${$data.progressBgColor} 0%)`
   } : {}, {
-    F: $data.showStartRecordingButton
+    G: $data.showStartRecordingButton
   }, $data.showStartRecordingButton ? {
-    G: common_vendor.o((...args) => $options.handleStartRecordingClick && $options.handleStartRecordingClick(...args))
+    H: common_vendor.o((...args) => $options.handleStartRecordingClick && $options.handleStartRecordingClick(...args))
   } : {}, {
-    H: $data.showRetryButton
+    I: $data.showRetryButton
   }, $data.showRetryButton ? {
-    I: common_vendor.o((...args) => $options.retryVideoUpload && $options.retryVideoUpload(...args))
+    J: common_vendor.o((...args) => $options.retryVideoUpload && $options.retryVideoUpload(...args))
   } : {}, {
-    J: $data.showCountdown
+    K: $data.showCountdown
   }, $data.showCountdown ? {
-    K: common_vendor.t($data.countdownValue)
+    L: common_vendor.t($data.countdownValue)
   } : {}, {
-    L: $data.showRerecordButton
+    M: $data.showRerecordButton
   }, $data.showRerecordButton ? {
-    M: common_vendor.o((...args) => $options.handleRerecordButtonClick && $options.handleRerecordButtonClick(...args))
-  } : {});
+    N: common_vendor.o((...args) => $options.handleRerecordButtonClick && $options.handleRerecordButtonClick(...args))
+  } : {}, {
+    O: $data.showPageWarning ? 1 : ""
+  });
 }
 const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-464e78c6"]]);
 wx.createPage(MiniProgramPage);

File diff ditekan karena terlalu besar
+ 0 - 0
unpackage/dist/dev/mp-weixin/pages/identity-verify/identity-verify.wxml


+ 41 - 3
unpackage/dist/dev/mp-weixin/pages/identity-verify/identity-verify.wxss

@@ -7,6 +7,25 @@
   display: flex;
   flex-direction: column;
   background-color: #f5f5f5;
+  transition: all 0.3s ease;
+  position: relative;
+}
+.page-warning.data-v-464e78c6 {
+  animation: page-warning-flash-464e78c6 1s ease-in-out infinite;
+}
+@keyframes page-warning-flash-464e78c6 {
+0% { 
+    background-color: rgba(255, 0, 0, 0.2);
+    box-shadow: 0 0 20px rgba(255, 0, 0, 0.3);
+}
+50% { 
+    background-color: rgba(255, 0, 0, 0.1);
+    box-shadow: 0 0 10px rgba(255, 0, 0, 0.2);
+}
+100% { 
+    background-color: rgba(255, 0, 0, 0.2);
+    box-shadow: 0 0 20px rgba(255, 0, 0, 0.3);
+}
 }
 .digital-human-container.data-v-464e78c6 {
   position: relative;
@@ -28,12 +47,31 @@
   position: absolute;
   top: 50px;
   right: 5px;
-  width: 110px; /* 稍微增加宽度 */
-  height: 160px; /* 稍微增加高度 */
-  border-radius: 4px; /* 减小圆角 */
+  width: 110px;
+  height: 160px;
+  border-radius: 4px;
   overflow: hidden;
   box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
   z-index: 20;
+  transition: all 0.3s ease;
+}
+.camera-warning.data-v-464e78c6 {
+  animation: camera-warning-flash-464e78c6 1s ease-in-out infinite;
+  border: 4px solid #ff0000;
+}
+@keyframes camera-warning-flash-464e78c6 {
+0% { 
+    border-color: #ff0000;
+    box-shadow: 0 0 15px rgba(255, 0, 0, 0.8);
+}
+50% { 
+    border-color: rgba(255, 0, 0, 0.3);
+    box-shadow: 0 0 5px rgba(255, 0, 0, 0.4);
+}
+100% { 
+    border-color: #ff0000;
+    box-shadow: 0 0 15px rgba(255, 0, 0, 0.8);
+}
 }
 
 /* 用户摄像头视频样式 */

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini