Bladeren bron

关联问题

yangg 2 maanden geleden
bovenliggende
commit
e3b40933d3

+ 4 - 2
src/views/questionBank/positionList/api.ts

@@ -1,6 +1,7 @@
 import { request } from '/@/utils/service';
 import { UserPageQuery, AddReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
 import { Session } from '/@/utils/storage';
+import { time } from 'console';
 
 
 export function GetDocumentList(query: UserPageQuery) {
@@ -81,11 +82,12 @@ export function DeleteDocumentCategory(id: any) {
 }
 
 // 添加获取面试问题的方法
-export function GetInterviewQuestions(params: any) {
+export function GetInterviewQuestions(params: any, timeout = 60000) {
     return request({
         url: '/api/system/interview_question/list',
         method: 'get',
-        params
+        params,
+        timeout
     });
 }
 

+ 172 - 0
src/views/questionBank/positionList/components/AsideTable/crud.ts

@@ -0,0 +1,172 @@
+import { CreateCrudOptionsProps, CreateCrudOptionsRet, dict, compute } from '@fast-crud/fast-crud';
+import * as api from '../../api';
+import { ElMessage } from 'element-plus';
+
+export const createAsideCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+  const pageRequest = async (query: any) => {
+    const params = {
+      ...query,
+      tenant_id: '1',
+      job_id: query.form?.job_id || ''
+    };
+    
+    return api.GetDocumentList(params);
+  };
+
+  return {
+    crudOptions: {
+      request: {
+        pageRequest
+      },
+      actionbar: {
+        buttons: {
+          add: {
+            show: true,
+            click: () => {
+              // 打开添加问题对话框
+              crudExpose.openAdd();
+            }
+          }
+        }
+      },
+      rowHandle: {
+        width: 300,
+        buttons: {
+          view: {
+            size: 'small',
+            icon: "View",
+            type: 'text',
+            click: ({ row }: any) => {
+              const event = new CustomEvent('viewDocumentDetail', { detail: row });
+              window.dispatchEvent(event);
+            }
+          },
+          uploadVideo: {
+            text: '上传视频',
+            type: 'text',
+            icon: "upload",
+            size: 'small',
+            click: ({ row }: any) => {
+              const event = new CustomEvent('openVideoUploadDialog', { detail: row });
+              window.dispatchEvent(event);
+            }
+          },
+          edit: {
+            type: 'text',
+            icon: "Edit",
+            size: 'small',
+          },
+          remove: {
+            type: 'text',
+            icon: "Delete",
+            size: 'small',
+          },
+          sort: {
+            type: 'text',
+            icon: "sort",
+            size: 'small',
+            text: '修改排序',
+            click: ({ row }: any) => {
+              const event = new CustomEvent('openSortDialog', { detail: row });
+              window.dispatchEvent(event);
+            }
+          }
+        }
+      },
+      columns: {
+        id: {
+          title: 'ID',
+          column: { show: true, width: 80 },
+          search: { show: false },
+          form: { show: false },
+        },
+        question: {
+          title: '题目内容',
+          search: { show: true },
+          column: {
+            sortable: 'custom',
+            showOverflowTooltip: true,
+            width: 300
+          },
+          form: {
+            rules: [{ required: true, message: '题目内容必填' }],
+            component: {
+              placeholder: '请输入题目内容',
+            }
+          },
+        },
+        question_type: {
+          title: '问题类型',
+          search: { show: true },
+          type: 'dict-select',
+          column: {
+            minWidth: 100,
+          },
+          dict: dict({
+            data: [
+              { value: 1, label: '专业技能' },
+              { value: 2, label: '通用能力' },
+              { value: 3, label: '个人特质' }
+            ]
+          }),
+          form: {
+            rules: [{ required: true, message: '问题类型必填' }],
+            component: {
+              placeholder: '请选择问题类型',
+            },
+            helper: '选择问题的类型分类'
+          },
+        },
+        difficulty: {
+          title: '难度等级',
+          search: { show: true },
+          type: 'dict-select',
+          column: {
+            minWidth: 80,
+          },
+          dict: dict({
+            data: [
+              { value: 1, label: '初级' },
+              { value: 2, label: '中级' },
+              { value: 3, label: '高级' }
+            ]
+          }),
+          form: {
+            rules: [{ required: true, message: '难度等级必填' }],
+            helper: '选择题目的难度级别'
+          },
+        },
+        sequence_number: {
+          title: '排序',
+          search: { show: false },
+          column: { show: true },
+          form: { show: true },
+        },
+        digital_human_video_url: {
+          title: '视频链接',
+          search: { show: false },
+          column: { 
+            show: true,
+            width: 120,
+            formatter: ({ row }: any) => {
+              return row.digital_human_video_url ? '已上传' : '未上传';
+            }
+          },
+          form: { show: false },
+        },
+        digital_human_video_subtitle: {
+          title: '字幕',
+          search: { show: false },
+          column: { 
+            show: true,
+            width: 120,
+            formatter: ({ row }: any) => {
+              return row.digital_human_video_subtitle ? '已上传' : '未上传';
+            }
+          },
+          form: { show: false },
+        },
+      }
+    }
+  };
+}; 

+ 68 - 0
src/views/questionBank/positionList/components/AsideTable/index.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="aside-table-container">
+    <div class="aside-header">
+      <h3>{{ currentPosition ? currentPosition.title + ' - 问题列表' : '请选择职位' }}</h3>
+    </div>
+    <fs-crud ref="asideCrudRef" v-bind="crudBinding"></fs-crud>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, watch } from 'vue';
+import { useFs } from '@fast-crud/fast-crud';
+import { createAsideCrudOptions } from './crud';
+import { ElMessage } from 'element-plus';
+
+const props = defineProps({
+  positionId: {
+    type: [String, Number],
+    default: ''
+  },
+  currentPosition: {
+    type: Object,
+    default: () => null
+  }
+});
+
+const emit = defineEmits(['refresh']);
+
+const { crudBinding, crudRef: asideCrudRef, crudExpose } = useFs({ createCrudOptions: createAsideCrudOptions });
+
+// 监听职位ID变化,刷新问题列表
+watch(() => props.positionId, (newVal) => {
+  if (newVal) {
+    loadQuestions(newVal);
+  }
+}, { immediate: true });
+
+// 加载问题列表
+const loadQuestions = (positionId) => {
+  if (!positionId) return;
+  
+  crudExpose.doSearch({
+    form: {
+      job_id: positionId
+    }
+  });
+};
+
+// 暴露方法给父组件
+defineExpose({
+  refresh: () => crudExpose.doRefresh(),
+  asideCrudRef
+});
+</script>
+
+<style lang="scss" scoped>
+.aside-table-container {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.aside-header {
+  padding: 10px 0;
+  border-bottom: 1px solid #ebeef5;
+  margin-bottom: 10px;
+}
+</style> 

+ 16 - 1
src/views/questionBank/positionList/crud.tsx

@@ -52,6 +52,21 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 				editRequest,
 				delRequest,
 			},
+			// 添加工具栏配置
+			actionbar: {
+				buttons: {
+					add: {
+						show: true,
+						text: '新增职位问题',
+						icon: 'Plus',
+						click: () => {
+							// 触发自定义事件,让父组件处理弹窗显示
+							const event = new CustomEvent('openPositionQuestionDialog');
+							window.dispatchEvent(event);
+						}
+					}
+				}
+			},
 			search: {
 				show: true,
 				onSearch: (params: any) => {
@@ -162,7 +177,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 						url: '/api/categories', // 获取数据的接口
 						// 或者使用方法
 						getData: async () => {
-						  const res = await api.GetInterviewQuestions({page:1,limit:1000,tenant_id:1});
+						  const res = await api.GetInterviewQuestions({page:1,limit:200,tenant_id:1,});
 						  return res.data.items;
 						},
 						label: 'question',

+ 376 - 7
src/views/questionBank/positionList/index.vue

@@ -232,22 +232,114 @@
         </span>
       </template>
     </el-dialog>
+
+    <!-- 添加职位问题对话框 -->
+    <el-dialog
+      v-model="positionQuestionDialogVisible"
+      title="职位问题"
+      width="60%"
+      destroy-on-close
+    >
+      <el-form :model="positionQuestionForm" label-width="80px">
+        <el-form-item label="职位">
+          <el-select v-model="positionQuestionForm.position" placeholder="请选择" style="width: 100%">
+            <el-option
+              v-for="item in positionOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <el-form-item label="职位问题">
+          <div class="question-selection-container">
+            <div class="question-search">
+              <el-input
+                v-model="questionSearchKeyword"
+                placeholder="搜索问题"
+                clearable
+                @input="filterQuestions"
+              >
+                <template #prefix>
+                  <el-icon><Search /></el-icon>
+                </template>
+              </el-input>
+            </div>
+            
+            <div class="question-table-container">
+              <el-table
+                v-loading="questionsLoading"
+                :data="filteredQuestionOptions"
+                style="width: 100%"
+                height="300"
+                @selection-change="handleQuestionSelectionChange"
+              >
+                <el-table-column type="selection" width="55" />
+                <el-table-column prop="label" label="问题" show-overflow-tooltip />
+                <el-table-column prop="question_form_name" label="问题类型" show-overflow-tooltip />
+                <el-table-column prop="chinese_explanation" label="描述" show-overflow-tooltip />
+              </el-table>
+              
+              <div class="pagination-container">
+                <el-pagination
+                  v-model:current-page="questionPagination.currentPage"
+                  v-model:page-size="questionPagination.pageSize"
+                  :page-sizes="[10, 20, 50, 100]"
+                  layout="total, sizes, prev, pager, next, jumper"
+                  :total="questionPagination.total"
+                  @size-change="handleQuestionSizeChange"
+                  @current-change="handleQuestionCurrentChange"
+                />
+              </div>
+            </div>
+          </div>
+        </el-form-item>
+        
+        <el-form-item label="排序">
+          <el-input v-model="positionQuestionForm.sequence" placeholder="请输入排序值" />
+        </el-form-item>
+      </el-form>
+      
+   <!--    <div class="question-list-container">
+        <h3>已选问题列表</h3>
+        <el-table :data="selectedQuestions" style="width: 100%">
+          <el-table-column prop="label" label="问题" show-overflow-tooltip />
+          <el-table-column prop="question_form_name" label="问题类型" show-overflow-tooltip />
+          <el-table-column prop="chinese_explanation" label="中文释义" show-overflow-tooltip />
+          <el-table-column label="操作" width="120">
+            <template #default="scope">
+              <el-button type="danger" link @click="removeQuestion(scope.row)">
+                <el-icon><Delete /></el-icon>
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div> -->
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="positionQuestionDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitPositionQuestionForm">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
   </fs-page>
 </template>
 
 <script lang="ts" setup name="documentList">
-import { ref, onMounted, onBeforeUnmount } from 'vue';
+import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
 import { useFs } from '@fast-crud/fast-crud';
 import XEUtils from 'xe-utils';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import { createCrudOptions } from './crud';
 import { Session } from '/@/utils/storage';
 import { getBaseURL } from '/@/utils/baseUrl';
-import { Document, Delete, View, Printer } from '@element-plus/icons-vue';
+import { Document, Delete, View, Printer, Search } from '@element-plus/icons-vue';
 import { successNotification } from '/@/utils/message';
 import DocumentTreeCom from './components/DocumentTreeCom/index.vue';
 import DocumentFormCom from './components/DocumentFormCom/index.vue';
-import { GetDocumentTree, DeleteDocumentCategory, UpdateDocument ,UpdateSequence} from './api';
+import { GetDocumentTree, DeleteDocumentCategory, UpdateDocument ,UpdateSequence, GetInterviewQuestions,AddDocument } from './api';
 import { useRouter } from 'vue-router';
 /* import { print } from '/@/utils/print'; */
 
@@ -274,7 +366,7 @@ const uploadedFile = ref<UploadedFileInfo | null>(null);
 
 // 树形结构相关数据
 const documentTreeData = ref([]);
-const documentTreeCacheData = ref([]);
+const documentTreeCacheData = ref<any[]>([]);
 const drawerVisible = ref(false);
 const drawerFormData = ref({});
 const documentTreeRef = ref<DocumentTreeRef | null>(null);
@@ -374,6 +466,211 @@ interface QuestionRow {
   sequence_number?: number;
 }
 
+// 职位问题对话框相关状态
+const positionQuestionDialogVisible = ref(false);
+const positionQuestionForm = ref({
+  position: '',
+  questions: [] as string[],
+  sequence: '0'
+});
+
+// 职位选项和问题选项
+const positionOptions = ref<{ value: string, label: string }[]>([]);
+const questionOptions = ref<{ value: string, label: string, chinese_explanation: string, question_form_name: string }[]>([]);
+
+// 计算属性:已选问题列表
+const selectedQuestions = computed(() => {
+  // 根据已选ID查找完整的问题对象
+  return positionQuestionForm.value.questions.map(questionId => {
+    // 先在当前页面的问题中查找
+    const question = questionOptions.value.find(q => q.value === questionId);
+    if (question) return question;
+    
+    // 如果在当前页面找不到,可能需要额外请求
+    // 这里简化处理,只返回ID
+    return { value: questionId, label: `问题 ${questionId}`, chinese_explanation: '' ,question_form_name: ''};
+  });
+});
+
+// 问题加载状态
+const questionsLoading = ref(false);
+
+// 问题分页参数
+const questionPagination = ref({
+  currentPage: 1,
+  pageSize: 10,
+  total: 0
+});
+
+// 加载问题选项
+const loadQuestionOptions = async () => {
+  try {
+    questionsLoading.value = true;
+    const response = await GetInterviewQuestions({
+      page: questionPagination.value.currentPage,
+      limit: questionPagination.value.pageSize,
+      tenant_id: 1,
+      keyword: questionSearchKeyword.value
+    });
+    
+    if (response && response.code === 2000 && response.data) {
+      // 转换数据格式
+      questionOptions.value = response.data.items.map((item: any) => ({
+        value: item.id.toString(),
+        label: item.question,
+        chinese_explanation: item.scoring_reference || '暂无释义',
+        question_form_name: item.question_form_name || '暂无问题类型'
+      }));
+      
+      // 更新总数
+      questionPagination.value.total = response.data.total;
+    } else {
+      ElMessage.error('获取问题列表失败');
+    }
+  } catch (error) {
+    console.error('加载问题选项失败:', error);
+    ElMessage.error('加载问题选项失败');
+  } finally {
+    questionsLoading.value = false;
+  }
+};
+
+// 处理问题选择变化
+const handleQuestionSelectionChange = (selection: any[]) => {
+  positionQuestionForm.value.questions = selection.map(item => item.value);
+};
+
+// 处理问题分页大小变化
+const handleQuestionSizeChange = (size: number) => {
+  questionPagination.value.pageSize = size;
+  loadQuestionOptions();
+};
+
+// 处理问题当前页变化
+const handleQuestionCurrentChange = (page: number) => {
+  questionPagination.value.currentPage = page;
+  loadQuestionOptions();
+};
+
+// 修改过滤问题的方法
+const filterQuestions = () => {
+  // 重置到第一页
+  questionPagination.value.currentPage = 1;
+  // 重新加载数据
+  loadQuestionOptions();
+};
+
+// 修改打开职位问题对话框方法
+const openPositionQuestionDialog = () => {
+  // 加载职位选项
+  loadPositionOptions();
+  // 加载问题选项
+  loadQuestionOptions();
+  positionQuestionDialogVisible.value = true;
+};
+
+// 修改 loadPositionOptions 函数,从实际API获取职位数据
+const loadPositionOptions = async () => {
+  try {
+    // 调用实际的API获取职位列表
+    const response = await fetch(getBaseURL() + 'api/system/job/list?tenant_id=1', {
+      method: 'GET',
+      headers: {
+        'Authorization': 'JWT ' + Session.get('token'),
+        'Content-Type': 'application/json'
+      }
+    });
+    
+    if (!response.ok) {
+      throw new Error('获取职位列表失败');
+    }
+    
+    const data = await response.json();
+    
+    if (data && data.code === 2000 && data.data) {
+      // 转换数据格式
+      positionOptions.value = data.data.map((item: any) => ({
+        value: item.id.toString(),
+        label: item.title,
+        // 如果有中文释义字段,可以在这里添加
+        description: item.description || ''
+      }));
+    } else {
+      ElMessage.error('获取职位列表失败');
+    }
+  } catch (error) {
+    console.error('加载职位选项失败:', error);
+    ElMessage.error('加载职位选项失败');
+  }
+};
+
+// 添加一个函数来获取职位名称
+const getPositionName = (positionId: string | number) => {
+  if (!positionId) return '';
+  const position = positionOptions.value.find(p => p.value === positionId.toString());
+  return position ? position.label : positionId.toString();
+};
+
+// 修改 submitPositionQuestionForm 函数,添加中文释义处理
+const submitPositionQuestionForm = async () => {
+  try {
+    if (!positionQuestionForm.value.position) {
+      ElMessage.warning('请选择职位');
+      return;
+    }
+    
+    if (positionQuestionForm.value.questions.length === 0) {
+      ElMessage.warning('请选择至少一个问题');
+      return;
+    }
+    
+    // 获取职位名称,用于显示成功消息
+    const positionName = getPositionName(positionQuestionForm.value.position);
+    
+    // 构建API请求数据 - 修改为符合API期望的格式
+    const requestData = {
+      position_id: parseInt(positionQuestionForm.value.position),
+      question_id: { 
+        question_id: positionQuestionForm.value.questions.map(questionId => parseInt(questionId)) 
+      },
+      duration: parseInt(positionQuestionForm.value.sequence) || 60,
+      tenant_id: 1
+    };
+    
+    console.log('提交的数据:', requestData);
+    
+    // 调用实际的API保存职位问题关联
+    const response = await AddDocument(requestData);
+    
+    if (response && response.code === 2000) {
+      ElMessage.success(`${positionName}职位问题保存成功`);
+      positionQuestionDialogVisible.value = false;
+      
+      // 重置表单
+      positionQuestionForm.value = {
+        position: '',
+        questions: [],
+        sequence: '0'
+      };
+      
+      // 刷新列表
+      crudExpose.doRefresh();
+    } else {
+      ElMessage.error(response?.msg || '保存失败');
+    }
+  } catch (error) {
+    console.error('保存职位问题失败:', error);
+    ElMessage.error('保存失败,请重试');
+  }
+};
+
+// 移除已选问题
+const removeQuestion = (question: { value: string, label: string }) => {
+  positionQuestionForm.value.questions = positionQuestionForm.value.questions.filter(
+    id => id !== question.value
+  );
+};
+
 // 获取文档树数据
 const getTreeData = () => {
   GetDocumentTree({}).then((ret: any) => {
@@ -477,9 +774,7 @@ const handleUploadSuccess = (response: any, uploadFile: any, scope: any) => {
   uploadedFile.value = fileInfo;
   
   // 刷新列表时传递category参数
-  crudExpose.doRefresh({
-    category: selectedCategoryId.value
-  });
+  crudExpose.doRefresh();
 };
 
 // 处理文件删除
@@ -921,6 +1216,7 @@ onMounted(() => {
   window.addEventListener('viewDocumentDetail', handleViewDetail);
   window.addEventListener('openVideoUploadDialog', handleOpenVideoUploadDialog);
   window.addEventListener('openSortDialog', handleOpenSortDialog);
+  window.addEventListener('openPositionQuestionDialog', openPositionQuestionDialog);
 });
 
 // 添加 onBeforeUnmount 钩子移除事件监听器
@@ -928,6 +1224,20 @@ onBeforeUnmount(() => {
   window.removeEventListener('viewDocumentDetail', handleViewDetail);
   window.removeEventListener('openVideoUploadDialog', handleOpenVideoUploadDialog);
   window.removeEventListener('openSortDialog', handleOpenSortDialog);
+  window.removeEventListener('openPositionQuestionDialog', openPositionQuestionDialog);
+});
+
+// 新增 questionSearchKeyword
+const questionSearchKeyword = ref('');
+const filteredQuestionOptions = computed(() => {
+  if (!questionSearchKeyword.value) {
+    return questionOptions.value;
+  }
+  
+  const keyword = questionSearchKeyword.value.toLowerCase();
+  return questionOptions.value.filter(item => 
+    item.label.toLowerCase().includes(keyword)
+  );
 });
 </script>
 
@@ -1067,4 +1377,63 @@ onBeforeUnmount(() => {
 .mt-2 {
   margin-top: 8px;
 }
+
+.question-list-container {
+  margin-top: 20px;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 15px;
+  background-color: #f8f9fa;
+}
+
+.question-list-container h3 {
+  margin-top: 0;
+  margin-bottom: 15px;
+  font-size: 16px;
+  color: #303133;
+}
+
+.question-selection-container {
+  width: 100%;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.question-search {
+  padding: 10px;
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.question-table-container {
+  display: flex;
+  flex-direction: column;
+  height: 350px;
+}
+
+.pagination-container {
+  margin-top: 10px;
+  padding: 5px 0;
+  display: flex;
+  justify-content: flex-end;
+  background-color: #f5f7fa;
+  border-top: 1px solid #ebeef5;
+}
+
+.question-item {
+  margin-bottom: 8px;
+  padding: 8px;
+  border-radius: 4px;
+  transition: background-color 0.2s;
+  
+  &:hover {
+    background-color: #f5f7fa;
+  }
+}
+
+.no-data {
+  padding: 20px 0;
+  text-align: center;
+}
 </style>