yangg 4 сар өмнө
parent
commit
78c645d0ae

+ 3 - 1
.env.development

@@ -1,9 +1,11 @@
 # 本地环境
 ENV = 'development'
 #https://minlong.raycos.com.cn
+#正式: https://backend.qicai321.com
 #线下:http://192.168.66.187:8083
 # 本地环境接口地址 121.36.251.245
-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/'
 
 # 是否启用按钮权限
 VITE_PM_ENABLED = true

+ 1 - 1
.env.production

@@ -2,7 +2,7 @@
 ENV = 'production'
 
 # 线上环境接口地址
-VITE_API_URL = 'https://minlong.raycos.com.cn' # docker-compose部署不需要修改,nginx容器自动代理了这个地址
+VITE_API_URL = 'https://backend.qicai321.com' # docker-compose部署不需要修改,nginx容器自动代理了这个地址
 
 # 是否启用按钮权限
 VITE_PM_ENABLED = true

+ 7 - 16
src/views/position/list/crud.tsx

@@ -57,23 +57,14 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 					remove: {
 						show: auth('role:Delete'),
 					},
-					/* qrcode: {
-						type: 'primary',
-						text: '扫码跳转',
-						icon: 'fa fa-qrcode',
-						show: true,
-						click: (clickContext: any): void => {
-							const { row } = clickContext;
-							// 生成微信小程序的跳转链接,包含职位ID
-							const miniProgramPath = `/pages/position/detail?id=${row.id}`;
-							// 打开二维码弹窗
-							context.openQRCodeDialog && context.openQRCodeDialog({
-								title: `${row.title} - 扫码查看`,
-								path: miniProgramPath,
-								positionId: row.id
-							});
+					qrcode: {
+						text: '小程序码',
+						icon: '',
+						click: (ctx) => {
+							context.generateQRCode(ctx.row);
 						},
-					}, */
+						order: 4
+					},
 					/* permission: {
 						type: 'primary',
 						text: '权限配置',

+ 72 - 3
src/views/position/list/index.vue

@@ -2,35 +2,104 @@
 	<fs-page>
 		<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
 		<PermissionDrawerCom />
+		<!-- 小程序二维码对话框 -->
+		<el-dialog v-model="qrCodeVisible" title="小程序二维码" width="400px" align-center>
+			<div class="qrcode-container">
+				<img v-if="qrCodeUrl" :src="qrCodeUrl" alt="小程序二维码" />
+				<div v-else class="loading">生成中...</div>
+			</div>
+		</el-dialog>
 	</fs-page>
 </template>
 
 <script lang="ts" setup name="role">
-import { defineAsyncComponent, onMounted } from 'vue';
+import { defineAsyncComponent, onMounted, ref } from 'vue';
 import { useFs } from '@fast-crud/fast-crud';
 import { createCrudOptions } from './crud';
 import { RoleDrawerStores } from './stores/RoleDrawerStores';
 import { RoleMenuBtnStores } from './stores/RoleMenuBtnStores';
 import { RoleMenuFieldStores } from './stores/RoleMenuFieldStores';
 import { RoleUsersStores } from './stores/RoleUsersStores';
+import { ElMessage } from 'element-plus';
+import axios from 'axios';
 
 const PermissionDrawerCom = defineAsyncComponent(() => import('./components/RoleDrawer.vue'));
 
+// 小程序二维码相关状态
+const qrCodeVisible = ref(false);
+const qrCodeUrl = ref('');
+
 const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
 const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单
 const RoleMenuField = RoleMenuFieldStores();// 角色-菜单-字段
 const RoleUsers = RoleUsersStores();// 角色-用户
 const { crudBinding, crudRef, crudExpose } = useFs({
 	createCrudOptions,
-	context: { RoleDrawer, RoleMenuBtn, RoleMenuField },
+	context: { 
+		RoleDrawer, 
+		RoleMenuBtn, 
+		RoleMenuField,
+		generateQRCode // 将二维码生成方法传递给crud配置
+	},
 });
 
+// 生成小程序二维码
+async function generateQRCode(row: { id: string | number }) {
+	try {
+		qrCodeVisible.value = true;
+		qrCodeUrl.value = ''; // 清空之前的二维码
+		
+		// 获取微信 access_token
+		const tokenResponse = await axios.get(`${import.meta.env.VITE_API_WX_URL}cgi-bin/token?grant_type=client_credential&appid=${'wxc9655eeaa3223b75'}&secret=${'c0f031b6e07ded1928fded435a913902'}`);
+		console.log(tokenResponse.data);
+		const accessToken = tokenResponse.data.access_token;
+		
+		if (!accessToken) {
+			throw new Error('获取微信 access_token 失败');
+		}
+		
+		// 调用后端API获取二维码
+		const response = await axios.post(`${import.meta.env.VITE_API_WX_URL}wxa/getwxacodeunlimit`, {
+			access_token: '91_vR3iqZvlk-YaoqEC3SBdDfITzvjeWzgkF9ybSIXMsR0Zvt7hSxnVgyVZXF0wXOOMqHxurxk_p5ggiZYVkDoueGuVGwJVU4NtZOb6q7Hxy035AhjOF0fN94-2DSUJQFeAFAFMB',
+			scene: `id=${row.id}`, // 传递角色ID或其他需要的参数
+			page: 'pages/position/detail', // 小程序中的页面路径
+			width: 430,
+			is_hyaline: true
+		}, {
+			responseType: 'blob' // 接收二进制数据
+		});
+		
+		// 将二进制数据转换为URL
+		const blob = new Blob([response.data], { type: 'image/png' });
+		qrCodeUrl.value = URL.createObjectURL(blob);
+	} catch (error) {
+		console.error('生成小程序码失败:', error);
+		ElMessage.error('生成小程序二维码失败,请稍后重试');
+		qrCodeVisible.value = false;
+	}
+}
+
 // 页面打开后获取列表数据
 onMounted(async () => {
 	// 刷新
 	crudExpose.doRefresh();
 	// 获取全部用户
 	RoleUsers.get_all_users();
-
 });
 </script>
+
+<style scoped>
+.qrcode-container {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	min-height: 300px;
+}
+.qrcode-container img {
+	max-width: 100%;
+}
+.loading {
+	font-size: 16px;
+	color: #909399;
+}
+</style>

+ 13 - 3
src/views/questionBank/positionList/api.ts

@@ -37,10 +37,11 @@ export function GetDocument(id: any) {
 }
 
 /* 文档管理删除 /api/system/document/{id}/*/
-export function DelDocument(id: any) {
+export function DelDocument(data:any) {
 	return request({
-		url: '/api/system/document/'+id+ '/',
-		method: 'delete',
+		url: 'api/system/job/delete_question',
+		method: 'post',
+		data
 	});
 }
 
@@ -86,4 +87,13 @@ export function GetInterviewQuestions(params: any) {
         method: 'get',
         params
     });
+}
+
+// 添加更新排序的API函数
+export function UpdateSequence(data:any) {
+  return request({
+    url: '/api/system/job/update_question_sequence_numbers',
+    method: 'post',
+    data
+  });
 }

+ 25 - 8
src/views/questionBank/positionList/crud.tsx

@@ -36,7 +36,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 	};
 
 	const delRequest = async ({ row }: any) => {
-		return await api.DelDocument(row.id);
+		return await api.DelDocument({id:row.id});
 	};
 
 	const addRequest = async ({ form }: any) => {
@@ -93,6 +93,16 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 					remove: {
 						type: 'danger',
 						size: 'small',
+					},
+					sort: {
+						type: 'warning',
+						size: 'small',
+						text: '修改排序',
+						click: ({ row }: any) => {
+							// 触发自定义事件,让父组件处理弹窗显示
+							const event = new CustomEvent('openSortDialog', { detail: row });
+							window.dispatchEvent(event);
+						}
 					}
 				}
 			},
@@ -218,7 +228,8 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 						data: [
 							{ value: 0, label: '开放问题' },
 							{ value: 1, label: '单选题' },
-							{ value: 2, label: '多选题' }
+							{ value: 2, label: '多选题' },
+							{ value: 3, label:"色盲题"}
 						]
 					}),
 					form: {
@@ -382,7 +393,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 						component: {
 							name: 'el-card',
 							children: {
-								default: ({ form }) => {
+								default: ({ form }: { form: any }) => {
 									// 确保options数组已初始化
 									if (!form.options) {
 										form.options = [];
@@ -480,6 +491,12 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 						})
 					}
 				},
+				sequence_number: {
+					title: '排序',
+					search: { show: false },
+					column: { show: true },
+					form: { show: true },
+				},
 				answer_explanation: {
 					title: '答案解析',
 					search: { show: false },
@@ -495,26 +512,26 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 					},
 				},
 				// 添加视频相关字段
-				video_url: {
+				digital_human_video_url: {
 					title: '视频链接',
 					search: { show: false },
 					column: { 
 						show: true,
 						width: 120,
 						formatter: ({ row }: any) => {
-							return row.video_url ? '已上传' : '未上传';
+							return row.digital_human_video_url ? '已上传' : '未上传';
 						}
 					},
 					form: { show: false },
 				},
-				subtitle_url: {
-					title: '字幕文件',
+				digital_human_video_subtitle: {
+					title: '字幕',
 					search: { show: false },
 					column: { 
 						show: true,
 						width: 120,
 						formatter: ({ row }: any) => {
-							return row.subtitle_url ? '已上传' : '未上传';
+							return row.digital_human_video_subtitle ? '已上传' : '未上传';
 						}
 					},
 					form: { show: false },

+ 170 - 27
src/views/questionBank/positionList/index.vue

@@ -25,7 +25,7 @@
                     Authorization: 'JWT ' + Session.get('token')
                   }"
                   :multiple="false" 
-                  :on-success="(response, file) => handleUploadSuccess(response, file, scope)" 
+                  :on-success="(response: any, file: any) => handleUploadSuccess(response, file, scope)" 
                   :drag="false" 
                   :show-file-list="false">
                   <el-button type="primary" icon="plus">上传</el-button>
@@ -200,6 +200,38 @@
         </span>
       </template>
     </el-dialog>
+    
+    <!-- 添加排序对话框 -->
+    <el-dialog
+      v-model="sortDialogVisible"
+      title="修改排序"
+      width="400px"
+      destroy-on-close
+    >
+      <el-form 
+        ref="sortFormRef" 
+        :model="sortForm" 
+        :rules="sortFormRules" 
+        label-width="100px"
+      >
+        <el-form-item label="排序值" prop="sequence_number">
+          <el-input-number 
+            v-model="sortForm.sequence_number" 
+            :min="0" 
+            :max="9999"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <div class="form-helper">数值越小,排序越靠前</div>
+      </el-form>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="sortDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitSortForm">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
   </fs-page>
 </template>
 
@@ -215,7 +247,7 @@ import { Document, Delete, View, Printer } 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 } from './api';
+import { GetDocumentTree, DeleteDocumentCategory, UpdateDocument ,UpdateSequence} from './api';
 import { useRouter } from 'vue-router';
 /* import { print } from '/@/utils/print'; */
 
@@ -223,15 +255,29 @@ const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
 
 const router = useRouter();
 
-// 存储已上传文件信息
-const uploadedFile = ref(null);
+// 添加 uploadedFile 类型定义
+interface UploadedFileInfo {
+  name: string;
+  file_path: string;
+  file_type: string;
+  file_size: number;
+  doc_type: string;
+  category: string;
+  status: boolean;
+  doc_desc: string;
+  version: string;
+  file: string;
+}
+
+// 修改 uploadedFile 的类型
+const uploadedFile = ref<UploadedFileInfo | null>(null);
 
 // 树形结构相关数据
 const documentTreeData = ref([]);
 const documentTreeCacheData = ref([]);
 const drawerVisible = ref(false);
 const drawerFormData = ref({});
-const documentTreeRef = ref(null);
+const documentTreeRef = ref<DocumentTreeRef | null>(null);
 
 // 添加一个响应式变量来存储当前选中的分类ID
 const selectedCategoryId = ref('');
@@ -274,13 +320,15 @@ const subtitleInfo = ref({
 
 // 视频上传对话框状态
 const videoUploadDialogVisible = ref(false);
-const currentRow = ref(null);
+const currentRow = ref<QuestionRow | null>(null);
 
 // 添加字幕内容输入框
 const subtitleContent = ref('');
 
 // 添加表单引用
-const videoFormRef = ref(null);
+const videoFormRef = ref<{
+  validate: (callback: (valid: boolean) => void) => void;
+} | null>(null);
 
 // 添加表单数据对象
 const videoForm = ref({
@@ -295,6 +343,37 @@ const videoFormRules = {
   ]
 };
 
+// 添加排序对话框相关状态
+const sortDialogVisible = ref(false);
+const sortForm = ref({
+  sequence_number: 0
+});
+const sortFormRules = {
+  sequence_number: [
+    { required: true, message: '请输入排序值', trigger: 'blur' },
+    { type: 'number', message: '排序值必须为数字', trigger: 'blur' }
+  ]
+};
+const sortFormRef = ref<any>(null);
+
+// 添加类型定义
+interface DocumentTreeRef {
+  treeRef?: {
+    currentNode?: {
+      parent?: {
+        data: any;
+      };
+    };
+  };
+}
+
+// 修改 currentRow 的类型
+interface QuestionRow {
+  id?: number;
+  question_id?: string | number;
+  sequence_number?: number;
+}
+
 // 获取文档树数据
 const getTreeData = () => {
   GetDocumentTree({}).then((ret: any) => {
@@ -333,7 +412,7 @@ const handleTreeClick = (record: any) => {
 // 处理文档分类的新增或编辑
 const handleUpdateDocument = (type: any, record: any) => {
   if (type === 'update' && record) {
-    const parentData = documentTreeRef.value?.treeRef?.currentNode.parent.data || {};
+    const parentData = documentTreeRef.value?.treeRef?.currentNode?.parent?.data || {};
     documentTreeCacheData.value = [parentData];
     drawerFormData.value = record;
   }
@@ -341,7 +420,7 @@ const handleUpdateDocument = (type: any, record: any) => {
 };
 
 // 关闭抽屉
-const handleDrawerClose = (type) => {
+const handleDrawerClose = (type: string) => {
   if (type === 'submit') {
     getTreeData();
   }
@@ -399,9 +478,7 @@ const handleUploadSuccess = (response: any, uploadFile: any, scope: any) => {
   
   // 刷新列表时传递category参数
   crudExpose.doRefresh({
-    form: {
-      category: selectedCategoryId.value
-    }
+    category: selectedCategoryId.value
   });
 };
 
@@ -453,15 +530,14 @@ const previewFile = (fileData: any) => {
     return;
   }
   
-  // 获取文件类型
   const fileType = fileData.file_type || '';
-  
+  const filePath = fileData.file;
   const previewUrl = `/#/preview?url=${encodeURIComponent(filePath)}&type=${fileType}`;
   window.open(previewUrl, '_blank');
 };
 
 // 处理查看详情
-const handleViewDetail = async (event) => {
+const handleViewDetail = async (event: any) => {
   const row = event.detail || event;
   try {
     // 可以选择直接使用传入的行数据,或者重新请求完整数据
@@ -478,7 +554,7 @@ const handleViewDetail = async (event) => {
 };
 
 // 获取文档类型名称
-const getDocTypeName = (docTypeId) => {
+const getDocTypeName = (docTypeId: number | string) => {
   if (!docTypeId) return '';
   const docTypeDict = [
     { value: 1, label: '合同文档' },
@@ -492,23 +568,23 @@ const getDocTypeName = (docTypeId) => {
 };
 
 // 获取分类名称
-const getCategoryName = (categoryId) => {
+const getCategoryName = (categoryId: number | string) => {
   if (!categoryId) return '';
   const categoryDict = (window as any).__categoryDict || [];
-  const category = categoryDict.find(item => item.id === categoryId);
+  const category = categoryDict.find((item: any) => item.id === categoryId);
   return category ? category.name : categoryId;
 };
 
 // 获取项目名称
-const getProjectName = (projectId) => {
+const getProjectName = (projectId: number | string) => {
   if (!projectId) return '';
   const projectDict = (window as any).__projectDict || [];
-  const project = projectDict.find(item => item.id === projectId);
+  const project = projectDict.find((item: any) => item.id === projectId);
   return project ? project.name : projectId;
 };
 
 // 预览详情中的文件
-const previewDetailFile = (fileData) => {
+const previewDetailFile = (fileData: any) => {
   if (!fileData || !fileData.file_path) {
     ElMessage.error('文件路径无效');
     return;
@@ -546,7 +622,7 @@ const handlePrint = () => {
 };
 
 // 视频上传前的验证
-const beforeVideoUpload = (file) => {
+const beforeVideoUpload = (file: any) => {
   const isVideo = file.type.startsWith('video/');
   const isLt500M = file.size / 1024 / 1024 < 500;
 
@@ -562,7 +638,7 @@ const beforeVideoUpload = (file) => {
 };
 
 // 字幕上传前的验证
-const beforeSubtitleUpload = (file) => {
+const beforeSubtitleUpload = (file: any) => {
   const validExtensions = ['.srt', '.vtt', '.ass'];
   const extension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
   const isValidType = validExtensions.includes(extension);
@@ -580,7 +656,7 @@ const beforeSubtitleUpload = (file) => {
 };
 
 // 修改视频上传成功处理函数
-const handleVideoUploadSuccess = (response, file) => {
+const handleVideoUploadSuccess = (response: any, file: any) => {
   console.log('response', response);
   console.log('file', file);
   
@@ -640,7 +716,7 @@ const handleVideoUploadSuccess = (response, file) => {
 };
 
 // 修改字幕上传成功处理函数
-const handleSubtitleUploadSuccess = (response, file) => {
+const handleSubtitleUploadSuccess = (response: any, file: any) => {
   if (response && response.data) {
     subtitleInfo.value = {
       url: response.data.bucket_url,
@@ -671,7 +747,7 @@ const handleUploadError = () => {
 };
 
 // 处理打开视频上传对话框
-const handleOpenVideoUploadDialog = (event) => {
+const handleOpenVideoUploadDialog = (event: any) => {
   const row = event.detail;
   if (row) {
     currentRow.value = row;
@@ -693,7 +769,7 @@ const handleOpenVideoUploadDialog = (event) => {
 const submitVideoForm = () => {
   if (!videoFormRef.value) return;
   
-  videoFormRef.value.validate(async (valid) => {
+  videoFormRef.value.validate(async (valid: boolean) => {
     if (valid) {
       try {
         if (!currentRow.value || !currentRow.value.id) {
@@ -749,6 +825,71 @@ const submitVideoForm = () => {
   });
 };
 
+// 处理打开排序对话框
+const handleOpenSortDialog = (event: any) => {
+  const row = event.detail;
+  if (row) {
+    currentRow.value = row;
+    sortForm.value.sequence_number = row.sequence_number || 0;
+    sortDialogVisible.value = true;
+  }
+};
+
+// 修改提交排序表单函数
+const submitSortForm = () => {
+  if (!sortFormRef.value) return;
+  
+  sortFormRef.value.validate(async (valid: boolean) => {
+    if (valid) {
+      try {
+        if (!currentRow.value || !currentRow.value.question_id) {
+          ElMessage.error('当前选中的题目信息无效');
+          return;
+        }
+
+        // 获取当前职位ID
+        const positionId = selectedCategoryId.value;
+        if (!positionId) {
+          ElMessage.error('未选择职位分类');
+          return;
+        }
+
+        // 构建符合新格式的更新数据
+        const updateData = {
+          position_id: positionId,
+          questions: [
+            {
+              question_id: currentRow.value.question_id,
+              sequence_number: sortForm.value.sequence_number
+            }
+          ],
+          tenant_id: 1
+        };
+
+        console.log('提交的排序数据:', updateData);
+        
+        const response = await UpdateSequence(updateData);
+        
+        if (response && response.code === 2000) {
+          ElMessage.success('排序修改成功');
+          sortDialogVisible.value = false;
+          
+          // 刷新列表
+          crudExpose.doRefresh();
+        } else {
+          ElMessage.error(response?.msg || '保存失败');
+        }
+      } catch (error) {
+        console.error('保存排序信息时出错:', error);
+        ElMessage.error('保存失败,请重试');
+      }
+    } else {
+      ElMessage.warning('请输入有效的排序值');
+      return false;
+    }
+  });
+};
+
 // 页面加载时获取数据
 onMounted(() => {
   getTreeData();
@@ -779,12 +920,14 @@ onMounted(() => {
   // 添加全局事件监听器
   window.addEventListener('viewDocumentDetail', handleViewDetail);
   window.addEventListener('openVideoUploadDialog', handleOpenVideoUploadDialog);
+  window.addEventListener('openSortDialog', handleOpenSortDialog);
 });
 
 // 添加 onBeforeUnmount 钩子移除事件监听器
 onBeforeUnmount(() => {
   window.removeEventListener('viewDocumentDetail', handleViewDetail);
   window.removeEventListener('openVideoUploadDialog', handleOpenVideoUploadDialog);
+  window.removeEventListener('openSortDialog', handleOpenSortDialog);
 });
 </script>