yangg 3 luni în urmă
părinte
comite
0975b76717

+ 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.66.187:8083'
 # VITE_API_URL = 'https://backend.qicai321.com'
 VITE_API_WX_URL='https://api.weixin.qq.com/'
 

+ 66 - 75
src/views/JobApplication/report/index.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, computed } from 'vue'
 import { message } from 'ant-design-vue'
 import type { FormInstance } from 'ant-design-vue'
 import { useRoute } from 'vue-router'
@@ -1277,6 +1277,36 @@ const getHandPhotos = (hand: 'left' | 'right'): PhotoResult[] => {
 
   return result;
 };
+
+// 在script setup部分添加以下函数
+const getScoreLevel = (score: number): string => {
+  if (score >= 90) return '优秀';
+  if (score >= 80) return '良好';
+  if (score >= 70) return '中等';
+  if (score >= 60) return '及格';
+  return '较差';
+}
+
+// 添加一个计算属性来获取competency_scores
+const competencyScores = computed(() => {
+  return apiData.value?.application?.comprehensive_analysis?.competency_scores || {};
+})
+
+// 添加一个函数来获取标签的背景色
+const getTagColorClass = (tagName: string): string => {
+  const colorMap: Record<string, string> = {
+    '沟通表达能力': 'bg-blue-100 text-blue-800',
+    '工作经历匹配度': 'bg-green-100 text-green-800', 
+    '操作技能水平': 'bg-yellow-100 text-yellow-800',
+    '质量意识': 'bg-indigo-100 text-indigo-800',
+    '安全意识': 'bg-pink-100 text-pink-800',
+    '团队协作能力': 'bg-purple-100 text-purple-800',
+    '问题解决能力': 'bg-orange-100 text-orange-800',
+    '学习能力': 'bg-red-100 text-red-800',
+    '初级统筹能力': 'bg-teal-100 text-teal-800'
+  };
+  return colorMap[tagName] || 'bg-gray-100 text-gray-800';
+}
 </script>
 
 <template>
@@ -1292,7 +1322,7 @@ const getHandPhotos = (hand: 'left' | 'right'): PhotoResult[] => {
         <div class="bg-[#1d1e3a] text-white p-4 rounded-t-lg">
           <div class="flex justify-between items-center">
             <div class="text-sm">No.{{ apiData?.application?.report_id || candidateInfo.idNumber }}</div>
-            <div class="text-sm">{{ new Date().toLocaleString() }}</div>
+            <div class="text-sm">{{ apiData?.application?.interview_duration_formatted || new Date().toLocaleString() }}</div>
           </div>
         </div>
 
@@ -1396,7 +1426,7 @@ const getHandPhotos = (hand: 'left' | 'right'): PhotoResult[] => {
           <div id="comprehensive-assessment" class="mb-8 pt-4 border-t border-transparent">
             <!-- 1. 综合评估 -->
             <div class="mb-8">
-              <h2 class="text-xl font-bold mb-6">1. 综合评估</h2>
+              <h1 class="font-bold mb-6">1. 综合评估</h1>
 
               <div class="space-y-4">
                 <!-- 为每个部分添加 id -->
@@ -1414,75 +1444,7 @@ const getHandPhotos = (hand: 'left' | 'right'): PhotoResult[] => {
                     <p class="text-gray-600 text-sm" style="font-size: 14px;">{{ candidateInfo.hireReason || '无详细说明' }}</p>
                   </div>
                 </div>
-                  <!-- 添加评分计算细节部分 -->
-                  <div id="score-calculation" class="border-b pb-4">
-                 <!--  <h3 class="font-semibold mb-2">评分详情</h3> -->
-                  <div class="bg-gray-50 p-4 rounded-lg space-y-3">
-                    <!-- 团队合作能力 -->
-                    <div class="flex items-start space-x-2" style="font-size: 14px;">
-                      <div style="font-size: 16px;" class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm whitespace-nowrap">团队合作能力</div>
-                      <div class="flex-1">
-                        <p class="text-gray-700">{{ candidateInfo.dimensions.teamwork }}</p>
-                      </div>
-                    </div>
-
-                    <!-- 学习能力 -->
-                    <div class="flex items-start space-x-2" style="font-size: 14px;">
-                      <div style="font-size: 16px;" class="bg-green-100 text-green-800 px-2 py-1 rounded text-sm whitespace-nowrap">学习能力</div>
-                      <div class="flex-1">
-                        <p class="text-gray-700">{{ candidateInfo.dimensions.learningAbility }}</p>
-                      </div>
-                    </div>
-
-                    <!-- 细致严谨 -->
-                    <div class="flex items-start space-x-2" style="font-size: 14px;">
-                      <div style="font-size: 16px;" class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-sm whitespace-nowrap">细致严谨</div>
-                      <div class="flex-1">
-                        <p class="text-gray-700">{{ candidateInfo.dimensions.attention }}</p>
-                      </div>
-                    </div>
-
-                    <!-- 工作适应性 -->
-                    <div class="flex items-start space-x-2" style="font-size: 14px;">
-                      <div style="font-size: 16px;" class="bg-indigo-100 text-indigo-800 px-2 py-1 rounded text-sm whitespace-nowrap">工作适应性</div>
-                      <div class="flex-1">
-                        <p class="text-gray-700">{{ candidateInfo.dimensions.workAdaptability }}</p>
-                      </div>
-                    </div>
-
-                    <!-- 服务意识 -->
-                    <div class="flex items-start space-x-2" style="font-size: 14px;">
-                      <div style="font-size: 16px;" class="bg-pink-100 text-pink-800 px-2 py-1 rounded text-sm whitespace-nowrap">服务意识</div>
-                      <div class="flex-1">
-                        <p class="text-gray-700">{{ candidateInfo.dimensions.serviceAwareness }}</p>
-                      </div>
-                    </div>
-
-                    <!-- 稳定性 -->
-                    <div class="flex items-start space-x-2" style="font-size: 14px;">
-                      <div style="font-size: 16px;" class="bg-purple-100 text-purple-800 px-2 py-1 rounded text-sm whitespace-nowrap">稳定性</div>
-                      <div class="flex-1">
-                        <p class="text-gray-700">优秀</p>
-                      </div>
-                    </div>
-
-                    <!-- 沟通表达 -->
-                    <div class="flex items-start space-x-2" style="font-size: 14px;">
-                      <div style="font-size: 16px;" class="bg-orange-100 text-orange-800 px-2 py-1 rounded text-sm whitespace-nowrap">沟通表达</div>
-                      <div class="flex-1">
-                        <p class="text-gray-700">优秀</p>
-                      </div>
-                    </div>
-
-                    <!-- 人脸一致性 (保留原有的) -->
-                    <div class="flex items-start space-x-2" style="font-size: 14px;">
-                      <div style="font-size: 16px;" class="bg-purple-100 text-purple-800 px-2 py-1 rounded text-sm whitespace-nowrap">人脸一致性</div>
-                      <div class="flex-1">
-                        <p class="text-gray-700">{{ apiData?.application?.comprehensive_analysis?.score_calculation?.face_consistency_score || '暂无数据' }}</p>
-                      </div>
-                    </div>
-                  </div>
-                </div>
+                  <!-- 替换原有的评分详情部分 -->
                 <div id="red-line" class="border-b pb-4">
                   <!-- 红线题检测部分的现有代码 -->
                   <div class="flex items-center justify-between mb-2">
@@ -1542,12 +1504,41 @@ const getHandPhotos = (hand: 'left' | 'right'): PhotoResult[] => {
                     <strong>录用建议:</strong> {{ getDuvHireRecommendation(apiData?.scoring_summary?.total_score_obtained) }}
                   </p>
                 </div>
+
+                <!-- 替换原有的评分详情部分 -->
+                <div id="score-calculation" class="border-b pb-4">
+                    <div class="rounded-lg space-y-3">
+                      <!-- 动态渲染competency_scores中的标签 -->
+                      <div v-for="(score, tagId) in competencyScores" 
+                           :key="tagId"
+                           class="flex items-start space-x-2" 
+                           style="font-size: 14px;">
+                        <div :class="[getTagColorClass(score.tag_name), 'px-2 py-1 rounded text-sm whitespace-nowrap']" 
+                             style="font-size: 14px;">
+                          {{ score.tag_name }}
+                        </div>
+                        <div class="flex-1">
+                          <p class="text-gray-700">
+                            {{ getScoreLevel(score.normalized_score) }}
+                           <!--  <span class="text-sm text-gray-500 ml-2">({{ score.normalized_score }}分)</span> -->
+                          </p>
+                        </div>
+                      </div>
+                      <!-- 人脸一致性 (保留原有的) -->
+                    <div class="flex items-start space-x-2" style="font-size: 14px;">
+                      <div style="font-size: 14px;" class="bg-purple-100 text-purple-800 px-2 py-1 rounded text-sm whitespace-nowrap">人脸一致性</div>
+                      <div class="flex-1">
+                        <p class="text-gray-700">{{ apiData?.application?.comprehensive_analysis?.score_calculation?.face_consistency_score || '暂无数据' }}</p>
+                      </div>
+                    </div>
+                    </div>
+                  </div>
               </div>
             </div>
           </div>
 
           <div id="image-analysis" class="mb-8 pt-4 border-t border-transparent">
-            <h2 class="text-xl font-bold mb-6">2. 图片分析评估</h2>
+            <h2 class=" font-bold mb-6">2. 图片分析评估</h2>
             <div class="space-y-4">
              
               
@@ -1566,7 +1557,7 @@ const getHandPhotos = (hand: 'left' | 'right'): PhotoResult[] => {
           </div>
 
           <div id="answer-records" class="mb-8 pt-4 border-t border-transparent">
-            <h2 class="text-xl font-bold mb-6">3. 回答记录</h2>
+            <h2 class="font-bold mb-6">3. 回答记录</h2>
             <div class="space-y-4">
               <div v-for="(record, index) in candidateInfo.interviewRecord" :key="index" class="bg-gray-50 rounded-lg p-4">
                 <!-- 题目标题部分 -->
@@ -1769,7 +1760,7 @@ const getHandPhotos = (hand: 'left' | 'right'): PhotoResult[] => {
           </div>
 
           <div id="other-info" class="mb-8 pt-4 border-t border-transparent">
-            <h2 class="text-xl font-bold mb-6">4. 其他信息</h2>
+            <h2 class="font-bold mb-6">4. 其他信息</h2>
             <div class="space-y-6">
               <!-- 验证状态列表 -->
               <div class="space-y-2">

+ 67 - 16
src/views/questionBank/list/crud.tsx

@@ -21,6 +21,11 @@ declare global {
  */
 export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
 	const pageRequest = async (query: any) => {
+		// 如果存在 question 搜索值,将其赋值给 keyword
+		if (query.question) {
+			query.keyword = query.question;
+			delete query.question; // 删除原有的 question 参数
+		}
 		return await api.GetList(query);
 	};
 	const editRequest = async ({ form, row }: EditReq) => {
@@ -102,7 +107,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 		const addResult = await api.AddObj(form);
 		
 		// 如果添加成功且题目形式为0(开放题)或5,调用数字人视频生成接口
-	/* 	if (addResult.code === 2000 && (form.question_form === 0 || form.question_form === 5)) {
+		/* if (addResult.code === 2000 && (form.question_form === 0 || form.question_form === 5)) {
 			try {
 				// 构造视频生成请求参数
 				const videoParams = {
@@ -352,17 +357,57 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 						type: 'text',
 						icon:"Delete",
 					},
-					/* permission: {
-						type: 'primary',
-						text: '权限配置',
-						show: auth('role:Permission'),
-						click: (clickContext: any): void => {
-							const { row } = clickContext;
-							context.RoleDrawer.handleDrawerOpen(row);
-							context.RoleMenuBtn.setState([]);
-							context.RoleMenuField.setState([]);
+					// 添加生成数字人视频按钮
+					generateVideo: {
+						text: '生成视频',
+						type: 'text',
+						size: 'small',
+						show: ({ row }) => {
+							// 只在题目形式为开放题(0)或追加型开放题(5)时显示
+							return row.question_form === 0 || row.question_form === 5;
 						},
-					}, */
+						click: async ({ row }) => {
+							try {
+								// 构造视频生成请求参数
+								const videoParams = {
+									video_url: "http://data.qicai321.com/minlong/32ae7e1f-042f-4ee5-b234-be34d98d70e6.mp4",
+									text: row.question,
+									question_id: row.id,
+									guidance_scale: 2.0,
+									inference_steps: 20,
+									seed: 1247
+								};
+
+								// 调用视频生成接口
+								const videoResult = await fetch('http://192.168.66.101:7861/queue/process/text/', {
+									method: 'POST',
+									headers: {
+										'Content-Type': 'application/json'
+									},
+									body: JSON.stringify(videoParams)
+								});
+
+								if (!videoResult.ok) {
+									console.warn('数字人视频生成请求失败:', await videoResult.text());
+									ElMessage({
+										type: 'error',
+										message: '视频生成请求失败'
+									});
+								} else {
+									ElMessage({
+										type: 'info',
+										message: '数字人视频正在生成中...'
+									});
+								}
+							} catch (error) {
+								console.error('调用数字人视频生成接口失败:', error);
+								ElMessage({
+									type: 'error',
+									message: '视频生成请求失败'
+								});
+							}
+						}
+					}
 				},
 			},
 			form: {
@@ -396,12 +441,18 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 					search: { 
 						show: true,
 						size: 'small',
-						col:{ span:3},
-
-					 },
+						col: { span: 3 },
+						component: {
+							name: 'el-input',
+							props: {
+								placeholder: '请输入题目内容搜索',
+								clearable: true
+							}
+						}
+					},
 					column: {
-						showOverflowTooltip: true, // 超出显示提示
-						width: 300 ,// 固定列宽
+						showOverflowTooltip: true,
+						width: 300,
 						sortable: 'custom',
 					},
 					form: {

+ 9 - 0
src/views/questionBank/positionList/api.ts

@@ -107,3 +107,12 @@ export function GetQuestionTypeList() {
 		method: 'get'
 	});
 }
+
+// 添加 UpdateInterviewQuestion 函数到 API 文件
+export const UpdateInterviewQuestion = (params: any) => {
+  return request({
+    url: '/api/system/interview_question/update',
+    method: 'post',
+    data: params
+  });
+};

+ 70 - 0
src/views/questionBank/positionList/crud.tsx

@@ -37,6 +37,15 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 		});
 	};
 
+	// 修改绑定parent_question_id的请求函数,使用现有的API
+	const updateParentQuestionRequest = async (id: number, parentQuestionId: number) => {
+		// 使用现有的UpdateDocument API而不是不存在的UpdateInterviewQuestion
+		return await api.UpdateDocument({
+			id: id,
+			parent_question_id: parentQuestionId
+		});
+	};
+
 	const delRequest = async ({ row }: any) => {
 		return await api.DelDocument({id:row.id});
 	};
@@ -141,6 +150,18 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 							window.dispatchEvent(event);
 						}
 					},
+					// 添加绑定中文释义按钮
+					bindParentQuestion: {
+						text: '绑定开放问题',
+						type: 'text',
+						icon: "Link",
+						size: 'small',
+						click: async ({ row }: any) => {
+							// 打开选择开放问题的对话框
+							const event = new CustomEvent('openBindParentQuestionDialog', { detail: row });
+							window.dispatchEvent(event);
+						}
+					},
 					edit: {
 						show:false,
 						type: 'text',
@@ -259,6 +280,55 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 						show: false
 					},
 				},
+				
+				// 添加 parent_question_id 字段
+				parent_question_id: {
+					title: '关联开放问题',
+					search: { show: true,
+						size: 'small',
+						col:{ span:3},
+					},
+					type: 'dict-select',
+					dict: dict({
+						url: '/api/open-questions', // 获取开放问题的接口
+						// 或者使用方法
+						getData: async () => {
+							const res = await api.GetInterviewQuestions({
+								page: 1,
+								limit: 200,
+								tenant_id: 1,
+								question_form: 0, // 开放问题类型
+							});
+							return res.data.items;
+						},
+						label: 'question',
+						value: 'id'
+					}),
+					column: {
+						minWidth: 150,
+						showOverflowTooltip: true,
+						formatter: ({ row }: any) => {
+							// 如果有关联问题,显示关联问题的内容
+							if (row.parent_question && row.parent_question.question) {
+								return row.parent_question.question;
+							} else if (row.parent_question_id) {
+								return `ID: ${row.parent_question_id}`;
+							}
+							return '无关联问题';
+						}
+					},
+					form: {
+						show: true,
+						component: {
+							props: {
+								filterable: true,
+								clearable: true,
+								placeholder: '请选择关联的开放问题',
+							}
+						},
+						helper: '关联到对应的开放问题,用于中文释义'
+					},
+				},
 				/* question_type: {
 					title: '问题类型',
 					search: { show: true },

+ 158 - 0
src/views/questionBank/positionList/index.vue

@@ -343,6 +343,53 @@
         </span>
       </template>
     </el-dialog>
+
+    <!-- 修改绑定开放问题的对话框 -->
+    <el-dialog
+      v-model="bindParentQuestionDialogVisible"
+      title="绑定开放问题"
+      width="50%"
+      :before-close="handleBindDialogClose"
+    >
+      <div>
+        <div class="search-container" style="margin-bottom: 15px;">
+          <el-input
+            v-model="openQuestionKeyword"
+            placeholder="搜索开放问题"
+            clearable
+            @input="searchOpenQuestions"
+            style="width: 100%"
+          >
+            <template #prefix>
+              <el-icon><Search /></el-icon>
+            </template>
+          </el-input>
+        </div>
+        
+        <p>请选择要关联的开放问题:</p>
+        <el-select
+          v-model="selectedParentQuestionId"
+          filterable
+          clearable
+          placeholder="请选择开放问题"
+          style="width: 100%"
+          :loading="openQuestionsLoading"
+        >
+          <el-option
+            v-for="item in openQuestionsList"
+            :key="item.id"
+            :label="item.question"
+            :value="item.id"
+          />
+        </el-select>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="handleBindDialogClose">取消</el-button>
+          <el-button type="primary" @click="confirmBindParentQuestion">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
   </fs-page>
 </template>
 
@@ -1293,6 +1340,8 @@ onMounted(() => {
   window.addEventListener('openVideoUploadDialog', handleOpenVideoUploadDialog);
   window.addEventListener('openSortDialog', handleOpenSortDialog);
   window.addEventListener('openPositionQuestionDialog', openPositionQuestionDialog);
+  // 添加绑定开放问题对话框的事件监听器
+  window.addEventListener('openBindParentQuestionDialog', handleOpenBindParentQuestionDialog as EventListener);
 });
 
 // 添加 onBeforeUnmount 钩子移除事件监听器
@@ -1301,10 +1350,115 @@ onBeforeUnmount(() => {
   window.removeEventListener('openVideoUploadDialog', handleOpenVideoUploadDialog);
   window.removeEventListener('openSortDialog', handleOpenSortDialog);
   window.removeEventListener('openPositionQuestionDialog', openPositionQuestionDialog);
+  // 移除绑定开放问题对话框的事件监听器
+  window.removeEventListener('openBindParentQuestionDialog', handleOpenBindParentQuestionDialog as EventListener);
 });
 
 // 新增 questionSearchKeyword
 const questionSearchKeyword = ref('');
+
+// 绑定开放问题相关变量
+const bindParentQuestionDialogVisible = ref(false);
+const selectedParentQuestionId = ref<number | null>(null);
+const openQuestionsList = ref<any[]>([]);
+const openQuestionsLoading = ref(false);
+const openQuestionKeyword = ref('');
+
+// 获取开放问题列表
+const fetchOpenQuestions = async () => {
+  try {
+    openQuestionsLoading.value = true;
+    
+    // 构建请求参数,添加关键词搜索
+    const params: any = {
+      page: 1,
+      limit: 200,
+      tenant_id: 1,
+      question_form: 0, // 开放问题类型
+    };
+    
+    // 如果有关键词,添加到请求参数
+    if (openQuestionKeyword.value) {
+      params.keyword = openQuestionKeyword.value;
+    }
+    
+    const res = await GetInterviewQuestions(params);
+    openQuestionsList.value = res.data.items || [];
+  } catch (error) {
+    console.error('获取开放问题列表失败:', error);
+    ElMessage.error('获取开放问题列表失败');
+  } finally {
+    openQuestionsLoading.value = false;
+  }
+};
+
+// 搜索开放问题
+const searchOpenQuestions = () => {
+  // 使用防抖函数,避免频繁请求
+  if (searchOpenQuestionsDebounce) {
+    clearTimeout(searchOpenQuestionsDebounce);
+  }
+  
+  searchOpenQuestionsDebounce = setTimeout(() => {
+    fetchOpenQuestions();
+  }, 300);
+};
+
+// 防抖变量
+let searchOpenQuestionsDebounce: number | null = null;
+
+// 监听打开绑定对话框事件
+const handleOpenBindParentQuestionDialog = (event: CustomEvent) => {
+  currentRow.value = event.detail;
+  // 如果已有关联的开放问题,则预选中
+  if (currentRow.value.parent_question_id) {
+    selectedParentQuestionId.value = currentRow.value.parent_question_id;
+  } else {
+    selectedParentQuestionId.value = null;
+  }
+  
+  // 重置搜索关键词
+  openQuestionKeyword.value = '';
+  
+  // 获取开放问题列表
+  fetchOpenQuestions();
+  
+  // 显示对话框
+  bindParentQuestionDialogVisible.value = true;
+};
+
+// 添加处理绑定对话框关闭的函数
+const handleBindDialogClose = () => {
+  bindParentQuestionDialogVisible.value = false;
+  selectedParentQuestionId.value = null;
+  currentRow.value = null;
+};
+
+// 添加确认绑定开放问题的函数
+const confirmBindParentQuestion = async () => {
+  if (!currentRow.value || selectedParentQuestionId.value === null) {
+    ElMessage.warning('请选择要关联的开放问题');
+    return;
+  }
+  
+  try {
+    await UpdateDocument({
+      id: currentRow.value.question_id,
+      parent_question_id: selectedParentQuestionId.value,
+      tenant_id: '1'
+    });
+    
+    ElMessage.success('绑定开放问题成功');
+    bindParentQuestionDialogVisible.value = false;
+    selectedParentQuestionId.value = null;
+    
+    // 刷新表格数据
+    crudExpose.doRefresh();
+  } catch (error) {
+    console.error('绑定开放问题失败:', error);
+    ElMessage.error('绑定开放问题失败');
+  }
+};
 </script>
 
 <style lang="scss" scoped>
@@ -1507,4 +1661,8 @@ const questionSearchKeyword = ref('');
   padding: 20px 0;
   text-align: center;
 }
+
+.search-container {
+  margin-bottom: 15px;
+}
 </style>

+ 2 - 2
src/views/system/home/index.vue

@@ -38,7 +38,7 @@
 				</div>
 			</el-col>
 		</el-row>
-		<!-- <el-row :gutter="15" class="home-card-three">
+		<el-row :gutter="15" class="home-card-three">
 			<el-col :xs="24" :sm="10" :md="10" :lg="8" :xl="8">
 				<div class="home-card-item">
 					<div class="home-card-item-title">快捷导航工具</div>
@@ -62,7 +62,7 @@
 					<div style="height: 100%" ref="homeBarRef"></div>
 				</div>
 			</el-col>
-		</el-row> -->
+		</el-row>
 	</div>
 </template>