yangg 1 dzień temu
rodzic
commit
6eaa457cee

+ 1 - 1
.env.development

@@ -6,7 +6,7 @@ ENV = 'development'
 # 本地环境接口地址 121.36.251.245
 
 
-VITE_API_URL = 'https://backend.qicai321.com'
+VITE_API_URL = 'http://192.168.100.187:8083'
 # VITE_API_URL = 'https://backend.qicai321.com'
 VITE_API_WX_URL='https://api.weixin.qq.com/'
 VITE_API_URL_MINLONG='http://117.185.80.170:7861'

+ 332 - 3
src/views/JobApplication/report/report.vue

@@ -71,6 +71,26 @@
         </div>
       </div>
 
+      <!-- 眼动情绪检测弹窗 -->
+      <el-dialog
+        v-model="eyeEmotionDialogVisible"
+        title="眼动情绪检测"
+        width="80%"
+        :before-close="handleEyeEmotionDialogClose"
+      >
+        <div class="eye-emotion-content">
+          <!-- 这里添加眼动情绪检测的内容 -->
+          <div class="emotion-data">
+            <h3>眼动追踪数据</h3>
+            <!-- 这里可以添加图表或详细数据 -->
+          </div>
+          <div class="emotion-analysis">
+            <h3>情绪分析结果</h3>
+            <!-- 这里可以添加分析结果 -->
+          </div>
+        </div>
+      </el-dialog>
+
       <!-- 修改导航选项卡部分 -->
       <div 
         class="nav-tabs"
@@ -256,7 +276,15 @@
       <div ref="cheat" class="detection-section">
         <div class="detection-section-header">
             <div class="section-title">作弊检测</div>
-            <div class="status-tag normal">无异常</div>
+            <div class="status-tag normal" v-if="true">无异常</div>
+            <el-button 
+              v-if="!true"
+              type="primary" 
+              link
+              @click="showCheatVerification = true"
+            >
+              查看异常详情
+            </el-button>
         </div>
      <div class="detection-section-content">
         <div class="detection-items">
@@ -840,6 +868,64 @@
 
  
 </el-dialog>
+
+<!-- 添加作弊检测详情弹窗 -->
+<el-dialog
+  title="作弊检测详情"
+  v-model="showCheatVerification"
+  width="80%"
+  :before-close="handleCheatVerificationClose"
+  class="cheat-verification-dialog"
+>
+  <div class="cheat-verification">
+    <div class="verification-content">
+      <!-- 转移、替考检测 -->
+      <div class="verification-item">
+        <div class="item-header">
+          <span class="item-title">转移、替考检测</span>
+          <span class="item-status normal">无风险</span>
+        </div>
+        <div class="item-details">
+          <p>通过AI人脸识别技术,对考生面部特征进行实时监控,未发现替考或转移行为。</p>
+          <div class="detail-images" v-if="false">
+            <!-- 如果有异常图片证据,这里可以展示 -->
+            <img src="" alt="异常证据" />
+          </div>
+        </div>
+      </div>
+
+      <!-- 读稿检测 -->
+      <div class="verification-item">
+        <div class="item-header">
+          <span class="item-title">读稿检测</span>
+          <span class="item-status normal">无风险</span>
+        </div>
+        <div class="item-details">
+          <p>通过眼动追踪和头部姿态分析,未发现考生有明显读稿行为。</p>
+          <div class="detail-images" v-if="false">
+            <!-- 如果有异常图片证据,这里可以展示 -->
+            <img src="" alt="异常证据" />
+          </div>
+        </div>
+      </div>
+
+      <!-- 录屏/切屏检测 -->
+      <div class="verification-item">
+        <div class="item-header">
+          <span class="item-title">录屏/切屏检测</span>
+          <span class="item-status normal">无风险</span>
+        </div>
+        <div class="item-details">
+          <p>系统监控显示考生未进行录屏或切屏操作,面试过程保持正常。</p>
+          <div class="detail-images" v-if="false">
+            <!-- 如果有异常图片证据,这里可以展示 -->
+            <img src="" alt="异常证据" />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</el-dialog>
 </div>
 </template>
 
@@ -900,6 +986,7 @@ export default {
         { id: 'cheat', name: '作弊检测' },
         { id: 'psychology', name: '心理检测' },
         { id: 'body', name: '肢体检测' },
+        { id: 'eye-emotion', name: '眼动/情绪检测', isDialog: true },
         { id: 'personal-info', name: '个人详细信息', isInfo: true },
         { id: 'answers', name: '问答记录' },
         { id: 'other-answers', name: '其余答题详情', isLink: true }
@@ -971,6 +1058,19 @@ export default {
         department: '',
       },
       showBodyVerification: false, // 添加控制弹窗显示的数据
+      eyeEmotionDialogVisible: false, // 控制眼动情绪检测弹窗显示
+      eyeEmotionData: {
+        eyeTrackingData: {
+          focusPoints: [],
+          dwellTime: [],
+          scanPath: []
+        },
+        emotionAnalysis: {
+          mainEmotion: '',
+          emotionDetails: [],
+          suggestions: []
+        }
+      },
       competencyMap: {
         'communication_cooperation': '团队协作能力',
         'learning_ability': '应变能力',
@@ -995,7 +1095,8 @@ export default {
       radarChartInstance: null,
       interviewRecords: [], // 存储问答记录
       followUpQuestions: [], // 存储追问题
-      noTableHeight: 0
+      noTableHeight: 0,
+      showCheatVerification: false, // 添加控制作弊检测详情弹窗显示
     }
   },
   async created() {
@@ -1441,10 +1542,51 @@ export default {
         this.showInfoDialog = true;
       } else if (tab.isLink) {
         this.scrollToSection(tab.id);
+      } else if (tab.isDialog) {
+        // 处理弹窗类型的标签点击
+        if (tab.id === 'eye-emotion') {
+          this.openEyeEmotionDialog();
+        }
       } else {
         this.scrollToSection(tab.id);
       }
     },
+
+    // 打开眼动情绪检测弹窗
+    async openEyeEmotionDialog() {
+      this.eyeEmotionDialogVisible = true;
+      /* await this.fetchEyeEmotionData(); */
+    },
+
+    // 关闭眼动情绪检测弹窗
+    handleEyeEmotionDialogClose() {
+      this.eyeEmotionDialogVisible = false;
+    },
+
+    // 获取眼动情绪数据
+    async fetchEyeEmotionData() {
+      try {
+        const application_id = this.$route.query.id || this.$route.params.id;
+        const response = await fetch(
+          `${import.meta.env.VITE_API_URL}/api/job/eye_emotion_analysis?tenant_id=${1}&application_id=${application_id}`
+        );
+
+        if (!response.ok) {
+          throw new Error('获取眼动情绪数据失败');
+        }
+
+        const data = await response.json();
+        
+        if (data.code === 2000) {
+          this.eyeEmotionData = data.data || this.eyeEmotionData;
+        } else {
+          ElMessage.error('获取眼动情绪数据失败');
+        }
+      } catch (error) {
+        console.error('获取眼动情绪数据失败:', error);
+        ElMessage.error('获取眼动情绪数据失败');
+      }
+    },
     handleDialogClose() {
       this.showInfoDialog = false;
     },
@@ -2127,6 +2269,9 @@ export default {
       // 其他情况返回默认文本
       return '暂无回答';
     },
+    handleCheatVerificationClose() {
+      this.showCheatVerification = false;
+    },
   }
 }
 </script>
@@ -2806,7 +2951,7 @@ export default {
 
     .tab-item {
       position: relative;
-      padding: 5px 22px;
+      padding: 5px 15px;
       font-size: 14px;
       color: #606266;
       cursor: pointer;
@@ -3684,6 +3829,112 @@ export default {
   border-radius: 12px;
   overflow: hidden;
 }
+
+// 眼动情绪检测弹窗样式
+.eye-emotion-content {
+  padding: 20px;
+
+  .emotion-data {
+    margin-bottom: 30px;
+
+    h3 {
+      font-size: 16px;
+      font-weight: bold;
+      color: #333;
+      margin-bottom: 20px;
+    }
+
+    .data-charts {
+      display: grid;
+      grid-template-columns: repeat(2, 1fr);
+      gap: 20px;
+      margin-bottom: 20px;
+
+      .chart-item {
+        background: #f8f8f8;
+        padding: 15px;
+        border-radius: 8px;
+
+        .chart-title {
+          font-size: 14px;
+          color: #666;
+          margin-bottom: 10px;
+        }
+      }
+    }
+  }
+
+  .emotion-analysis {
+    h3 {
+      font-size: 16px;
+      font-weight: bold;
+      color: #333;
+      margin-bottom: 20px;
+    }
+
+    .analysis-content {
+      background: #f8f8f8;
+      padding: 20px;
+      border-radius: 8px;
+
+      .main-emotion {
+        font-size: 14px;
+        color: #333;
+        margin-bottom: 15px;
+      }
+
+      .emotion-details {
+        margin-bottom: 20px;
+
+        .detail-item {
+          margin-bottom: 10px;
+          padding: 10px;
+          background: #fff;
+          border-radius: 6px;
+          
+          .emotion-type {
+            font-weight: 500;
+            color: #333;
+            margin-bottom: 5px;
+          }
+
+          .emotion-desc {
+            color: #666;
+            font-size: 13px;
+          }
+        }
+      }
+
+      .suggestions {
+        .suggestion-title {
+          font-weight: 500;
+          color: #333;
+          margin-bottom: 10px;
+        }
+
+        .suggestion-list {
+          list-style: none;
+          padding: 0;
+
+          li {
+            color: #666;
+            font-size: 13px;
+            margin-bottom: 8px;
+            padding-left: 15px;
+            position: relative;
+
+            &:before {
+              content: "•";
+              position: absolute;
+              left: 0;
+              color: #333;
+            }
+          }
+        }
+      }
+    }
+  }
+}
 // 移动端适配
 @media screen and (max-width: 768px) {
   .qa-section {
@@ -3911,4 +4162,82 @@ export default {
     }
   }
 }
+
+/* 作弊检测弹窗样式 */
+.cheat-verification-dialog {
+  :deep(.el-dialog__body) {
+    padding: 20px;
+  }
+}
+
+.cheat-verification {
+  .verification-content {
+    .verification-item {
+      background: #f8f8f8;
+      border-radius: 8px;
+      padding: 16px;
+      margin-bottom: 16px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
+      .item-header {
+        display: flex;
+        align-items: center;
+        margin-bottom: 12px;
+
+        .item-title {
+          font-size: 16px;
+          font-weight: 500;
+          color: #333;
+          margin-right: 12px;
+        }
+
+        .item-status {
+          padding: 4px 12px;
+          border-radius: 4px;
+          font-size: 14px;
+
+          &.normal {
+            background: #e1f3d8;
+            color: #67c23a;
+          }
+
+          &.warning {
+            background: #fdf6ec;
+            color: #e6a23c;
+          }
+
+          &.error {
+            background: #fef0f0;
+            color: #f56c6c;
+          }
+        }
+      }
+
+      .item-details {
+        p {
+          color: #666;
+          font-size: 14px;
+          line-height: 1.6;
+          margin: 0;
+        }
+
+        .detail-images {
+          margin-top: 12px;
+          display: flex;
+          gap: 12px;
+
+          img {
+            width: 120px;
+            height: 90px;
+            object-fit: cover;
+            border-radius: 4px;
+          }
+        }
+      }
+    }
+  }
+}
 </style>

+ 1 - 1
src/views/position/create/index.vue

@@ -176,7 +176,7 @@
         </el-form-item> -->
         
         <el-form-item label="职位状态" prop="status">
-          <el-select v-model="formData.status" placeholder="请选择职位状态">
+          <el-select v-model="formData.status" disabled placeholder="请选择职位状态">
             <el-option label="未发布" :value="0" />
             <el-option label="已发布" :value="1" />
             <!-- <el-option label="已结束" :value="2" /> -->

+ 9 - 0
src/views/position/detail/api.ts

@@ -213,3 +213,12 @@ export function GetPositionList(query: UserPageQuery) {
 		params: {...query,tenant_id:1},
 	});
 }
+
+/* 检测是否有题目可以发布 */
+export function CheckQuestionVideosStatus(data:any) {
+	return request({
+		url: 'api/system/job/question_videos_status',
+		method: 'get',
+		params: data,
+	});
+}

+ 27 - 0
src/views/position/detail/index.vue

@@ -2925,6 +2925,33 @@ const handleStatusChange = async (value: boolean) => {
   const id = route.query.id;
   try {
     const newStatus = value ? 1 : 0;
+
+    // 如果是要开启职位,需要先检查视频状态
+    if (value) {
+      // 检查视频状态
+      const videoStatusResponse = await api.CheckQuestionVideosStatus({
+        position_id: id,
+        tenant_id: 1
+      });
+
+      if (videoStatusResponse.code === 2000) {
+        const { is_all_completed, total_questions } = videoStatusResponse.data;
+        
+        // 检查视频是否全部完成且存在题目
+        if (!is_all_completed || total_questions <= 0) {
+          ElMessage.warning(`无法开启职位,原因:${total_questions <= 0 ? '当前职位没有设置面试题目' : '面试题目视频未全部生成完成'}`);
+          // 恢复原状态
+          positionStatus.value = !value;
+          return;
+        }
+      } else {
+        ElMessage.error('检查视频状态失败,请稍后重试');
+        // 恢复原状态
+        positionStatus.value = !value;
+        return;
+      }
+    }
+
     await api.UpdateObj({
       id: id,
       status: newStatus

+ 11 - 1
src/views/position/list/api.ts

@@ -100,4 +100,14 @@ export function GeneratePositionDescription(data:any) {
 			'Authorization': 'JWT ' +Session.get('token')
 		}
 	});
-}
+}
+
+/* 检测是否有题目可以发布 */
+export function CheckQuestionVideosStatus(data:any) {
+	return request({
+		url: 'api/system/job/question_videos_status',
+		method: 'get',
+		params: data,
+	});
+}
+

+ 56 - 3
src/views/position/list/components/BatchStatusDialog.vue

@@ -32,7 +32,9 @@
 <script lang="ts" setup>
 import { ref, reactive } from 'vue';
 import { useBatchUpdateStatus } from '../crud';
-import { warningMessage } from '../../../../utils/message';
+import { warningMessage, successMessage } from '../../../../utils/message';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import * as api from '../api';
 
 const props = defineProps({
   crudExpose: {
@@ -69,8 +71,59 @@ const handleConfirm = async () => {
   try {
     // 提取所有选中职位的ID
     const positionIds = selectedPositions.value.map(item => item.id);
-    await batchUpdateStatus(positionIds, form.status);
-    dialogVisible.value = false;
+
+    // 如果是要发布职位,需要先检查视频状态
+    if (form.status === 1) { // 发布状态
+      // 检查每个选中职位的视频状态
+      for (const position of selectedPositions.value) {
+        const response = await api.CheckQuestionVideosStatus({
+          position_id: position.id,
+          tenant_id: 1
+        });
+
+        if (response.code === 2000) {
+          const { is_all_completed, total_questions } = response.data;
+          
+          // 检查视频是否全部完成且存在题目
+          if (!is_all_completed || total_questions <= 0) {
+            const reason = total_questions <= 0 ? '当前职位没有设置面试题目' : '面试题目视频未全部生成完成';
+            await ElMessageBox.alert(
+              `无法发布职位 "${position.title}",原因:\n${reason}`,
+              '无法发布',
+              {
+                confirmButtonText: '确定',
+                type: 'warning',
+              }
+            );
+            loading.value = false;
+            return;
+          }
+        } else {
+          ElMessage.error('检查视频状态失败,请稍后重试');
+          loading.value = false;
+          return;
+        }
+      }
+
+      // 所有职位检查通过后,调用批量发布API
+      const publishResponse = await api.batch_publish({
+        ids: positionIds,
+        tenant_id: 1
+      });
+
+      if (publishResponse.code === 2000) {
+        successMessage('职位批量发布成功');
+        dialogVisible.value = false;
+        props.crudExpose.doRefresh();
+      } else {
+        ElMessage.error(publishResponse.msg || '发布失败');
+      }
+    } else {
+      // 其他状态直接调用批量更新API
+      await batchUpdateStatus(positionIds, form.status);
+      dialogVisible.value = false;
+      props.crudExpose.doRefresh();
+    }
   } finally {
     loading.value = false;
   }

+ 26 - 0
src/views/position/list/index.vue

@@ -141,6 +141,32 @@ async function publishPosition(row: any) {
 			}
 		}
 
+		// 检查视频状态
+		const videoStatusResponse = await api.CheckQuestionVideosStatus({
+			position_id: row.id,
+			tenant_id: 1
+		});
+
+		if (videoStatusResponse.code === 2000) {
+			const { is_all_completed, total_questions } = videoStatusResponse.data;
+			
+			// 检查视频是否全部完成且存在题目
+			if (!is_all_completed || total_questions <= 0) {
+				await ElMessageBox.alert(
+					`无法${action}职位,原因:\n${total_questions <= 0 ? '当前职位没有设置面试题目' : '面试题目视频未全部生成完成'}`,
+					'无法发布',
+					{
+						confirmButtonText: '确定',
+						type: 'warning',
+					}
+				);
+				return;
+			}
+		} else {
+			ElMessage.error('检查视频状态失败,请稍后重试');
+			return;
+		}
+
 		const confirmText = row.status === 2 
 			? `确定要重新发布职位 "${row.title}" 吗?`
 			: `确定要发布职位 "${row.title}" 吗?发布后求职者即可看到此职位。`;