yangg 2 ماه پیش
والد
کامیت
62a0acdbc2

+ 2 - 2
src/settings.ts

@@ -54,8 +54,8 @@ export default {
                                 if (res.data.items && Array.isArray(res.data.items)) {
                                     return {
                                         records: res.data.items,
-                                        currentPage: res.page || 1,
-                                        pageSize: res.limit || 20,
+                                        currentPage: res.page||res.data.page || 1,
+                                        pageSize: res.limit || res.data.limit || 20,
                                         total: res.data.total || res.total || res.data.items.length
                                     };
                                 }

+ 235 - 138
src/views/JobApplication/report/index.vue

@@ -3,6 +3,8 @@ import { ref, onMounted } from 'vue'
 import { message } from 'ant-design-vue'
 import type { FormInstance } from 'ant-design-vue'
 import { useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { Share, Download, Printer, ArrowUp } from '@element-plus/icons-vue'
 
 interface CandidateInfo {
   name: string
@@ -68,6 +70,10 @@ interface CandidateInfo {
       images: string[]
     }
   }
+  strengths?: string[]
+  weaknesses?: string[]
+  hireRecommendation?: string
+  hireReason?: string
 }
 
 const candidateInfo = ref<CandidateInfo>({
@@ -169,7 +175,11 @@ const candidateInfo = ref<CandidateInfo>({
        
       ]
     }
-  }
+  },
+  strengths: [],
+  weaknesses: [],
+  hireRecommendation: '',
+  hireReason: ''
 })
 
 const apiData = ref<any>(null)
@@ -262,22 +272,41 @@ const updateCandidateInfo = (data: any) => {
     candidateInfo.value.suggestedSalary = '面议'
   }
   
-  // 更新综合评分
-  if (application?.comprehensive_score !== null && application?.comprehensive_score !== undefined) {
-    candidateInfo.value.score = application.comprehensive_score
-  } else {
-    // 如果没有综合评分,计算面试题目的平均分
-    const answeredQuestions = interview_progress?.filter((q: any) => 
-      q.video_answer && q.video_answer.ai_score
-    ) || []
+  // 更新综合分析
+  if (application?.comprehensive_analysis) {
+    // 更新综合评分
+    if (application.comprehensive_analysis.comprehensive_score !== null && 
+        application.comprehensive_analysis.comprehensive_score !== undefined) {
+      candidateInfo.value.score = application.comprehensive_analysis.comprehensive_score
+    }
     
-    if (answeredQuestions.length > 0) {
-      const totalScore = answeredQuestions.reduce((sum: number, q: any) => 
-        sum + (q.video_answer.ai_score || 0), 0
-      )
-      candidateInfo.value.score = Math.round(totalScore / answeredQuestions.length)
-    } else {
-      candidateInfo.value.score = 0
+    // 更新优缺点分析
+    if (application.comprehensive_analysis.video_analysis_data) {
+      const videoData = application.comprehensive_analysis.video_analysis_data
+      
+      // 更新优点
+      if (videoData.strengths && videoData.strengths.length > 0) {
+        candidateInfo.value.strengths = videoData.strengths.filter((s: string) => 
+          s && !s.includes('无法从响应中提取')
+        )
+      }
+      
+      // 更新缺点
+      if (videoData.weaknesses && videoData.weaknesses.length > 0) {
+        candidateInfo.value.weaknesses = videoData.weaknesses.filter((w: string) => 
+          w && !w.includes('无法从响应中提取')
+        )
+      }
+      
+      // 更新录用建议
+      if (application.comprehensive_analysis.hire_recommendation) {
+        candidateInfo.value.hireRecommendation = application.comprehensive_analysis.hire_recommendation
+      }
+      
+      // 更新录用理由
+      if (application.comprehensive_analysis.hire_reason) {
+        candidateInfo.value.hireReason = application.comprehensive_analysis.hire_reason
+      }
     }
   }
   
@@ -404,7 +433,41 @@ const updateCandidateInfo = (data: any) => {
   }
   
   // 更新DUV分析
-  if (application?.visual_analysis_results && application.visual_analysis_results.detections) {
+  if (application?.visual_analysis_results && application.visual_analysis_results.photo_results) {
+    // 从photo_results中收集所有detections
+    const allDetections: any[] = []
+    
+    application.visual_analysis_results.photo_results.forEach((photo: any) => {
+      if (photo.detections && photo.detections.length > 0) {
+        photo.detections.forEach((detection: any) => {
+          allDetections.push({
+            title: detection.feature || '特征分析',
+            content: detection.location ? 
+              `在${detection.location}发现${detection.feature}${detection.description ? ',' + detection.description : ''}` : 
+              detection.feature + (detection.description ? ',' + detection.description : ''),
+            score: detection.confidence >= 0.8 ? '确认' : '疑似',
+            type: detection.description && detection.description.includes('影响') ? 'negative' : 'neutral'
+          })
+        })
+      }
+    })
+    
+    // 如果有检测结果,更新DUV分析
+    if (allDetections.length > 0) {
+      candidateInfo.value.duvAnalysis = allDetections
+    } else {
+      // 如果没有检测结果,提供默认值
+      candidateInfo.value.duvAnalysis = [
+        {
+          title: '未发现特殊特征',
+          content: '未在照片中检测到特殊特征',
+          score: '正常',
+          type: 'positive'
+        }
+      ]
+    }
+  } else if (application?.visual_analysis_results && application.visual_analysis_results.detections) {
+    // 兼容旧版API格式
     candidateInfo.value.duvAnalysis = application.visual_analysis_results.detections.map((detection: any) => ({
       title: detection.feature || '特征分析',
       content: detection.location ? `在${detection.location}发现${detection.feature}` : detection.feature,
@@ -415,15 +478,9 @@ const updateCandidateInfo = (data: any) => {
     // 如果没有视觉分析结果,提供默认值
     candidateInfo.value.duvAnalysis = [
       {
-        title: '',
-        content: '',
-        score: '',
-        type: 'neutral'
-      },
-      {
-        title: '',
-        content: '',
-        score: '',
+        title: '未进行DUV分析',
+        content: '未提供DUV分析数据',
+        score: '未知',
         type: 'neutral'
       }
     ]
@@ -512,19 +569,43 @@ const handleSubmit = async () => {
   }
 }
 
+const reportTop = ref<HTMLElement | null>(null)
+
 const scrollToTop = () => {
-  window.scrollTo({
-    top: 0,
-    behavior: 'smooth'
-  })
+  console.log('尝试滚动到顶部')
+  
+  // 使用ref引用直接滚动到顶部元素
+  if (reportTop.value) {
+    reportTop.value.scrollIntoView({ behavior: 'smooth', block: 'start' })
+    console.log('使用ref滚动到顶部')
+    return
+  }
+  
+  // 如果ref不可用,尝试通过ID查找元素
+  const topElement = document.getElementById('report-top')
+  if (topElement) {
+    topElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
+    console.log('使用ID滚动到顶部')
+    return
+  }
+  
+  // 如果以上方法都失败,使用之前的备用方法
+  // ... 之前的代码 ...
 }
 
 const handleShare = () => {
-  message.success('分享链接已复制')
+  ElMessage.success('分享链接已复制')
 }
 
 const handleDownload = () => {
-  message.success('报告下载中...')
+  ElMessage.success('报告下载中...')
+}
+
+const handlePrint = () => {
+  ElMessage.success('准备打印报告...')
+  setTimeout(() => {
+    window.print()
+  }, 300)
 }
 
 // 添加base64编码的内联图片作为fallback
@@ -627,6 +708,9 @@ const handleVideoError = (event: Event) => {
 
 <template>
   <div class="max-w-4xl mx-auto p-6 relative overflow-y-auto h-full">
+    <!-- 添加顶部锚点 -->
+    <div id="report-top" ref="reportTop"></div>
+    
     <!-- 加载状态 -->
     <a-spin :spinning="loading" tip="加载中...">
       <!-- 页面标题 -->
@@ -694,33 +778,45 @@ const handleVideoError = (event: Event) => {
           </div>
         </div>
 
-        <!-- AI维度分析 1-->
-        <!-- <div class="mb-8">
-          <h2 class="text-xl font-bold mb-6">1. AI维度分析</h2>
-          <div class="space-y-6">
-            <div v-for="(value, key) in candidateInfo.dimensions" :key="key" class="border-b pb-4">
-              <div class="flex items-center mb-2">
-                <span class="text-gray-600 w-32">{{ {
-                  teamwork: '团队合作能力',
-                  learningAbility: '学习能力',
-                  attention: '细致严谨',
-                  workAdaptability: '工作适应性',
-                  serviceAwareness: '服务意识'
-                }[key] }}</span>
-                <span class="ml-4" :class="{
-                  'text-red-500': value === '欠佳',
-                  'text-green-500': value === '优秀',
-                  'text-yellow-500': value === '中等'
-                }">{{ value }}</span>
+        <!-- 1. 综合评估 -->
+        <div class="mb-8">
+          <h2 class="text-xl font-bold mb-6">1. 综合评估</h2>
+          <div class="space-y-4">
+            <div class="border-b pb-4">
+              <div class="flex items-center justify-between mb-2">
+                <span class="text-gray-600">录用建议</span>
+                <span :class="{
+                  'text-green-500': candidateInfo.hireRecommendation?.includes('推荐'),
+                  'text-red-500': candidateInfo.hireRecommendation?.includes('不推荐'),
+                  'text-yellow-500': !candidateInfo.hireRecommendation?.includes('推荐') && !candidateInfo.hireRecommendation?.includes('不推荐')
+                }">{{ candidateInfo.hireRecommendation || '无建议' }}</span>
               </div>
-              <p class="text-gray-600 text-sm">{{ candidateInfo.dimensionDetails[key] }}</p>
+              <p class="text-gray-600 text-sm">{{ candidateInfo.hireReason || '无详细说明' }}</p>
+            </div>
+            
+            <div v-if="candidateInfo.strengths && candidateInfo.strengths.length > 0" class="border-b pb-4">
+              <h3 class="font-semibold mb-2">优点</h3>
+              <ul class="list-disc pl-5 text-gray-600 text-sm">
+                <li v-for="(strength, index) in candidateInfo.strengths" :key="'strength-'+index">
+                  {{ strength }}
+                </li>
+              </ul>
+            </div>
+            
+            <div v-if="candidateInfo.weaknesses && candidateInfo.weaknesses.length > 0" class="border-b pb-4">
+              <h3 class="font-semibold mb-2">需改进的地方</h3>
+              <ul class="list-disc pl-5 text-gray-600 text-sm">
+                <li v-for="(weakness, index) in candidateInfo.weaknesses" :key="'weakness-'+index">
+                  {{ weakness }}
+                </li>
+              </ul>
             </div>
           </div>
-        </div> -->
+        </div>
 
-        <!-- DUV分析评估 -->
+        <!-- 2. DUV分析评估 -->
         <div class="mb-8">
-          <h2 class="text-xl font-bold mb-6">1. DUV分析评估</h2>
+          <h2 class="text-xl font-bold mb-6">2. DUV分析评估</h2>
           <div class="space-y-4">
             <div v-for="(item, index) in candidateInfo.duvAnalysis" :key="index" class="border-b pb-4">
               <div class="flex items-center justify-between mb-2">
@@ -736,9 +832,9 @@ const handleVideoError = (event: Event) => {
           </div>
         </div>
 
-        <!-- 面试记录 -->
+        <!-- 3. 面试记录 -->
         <div class="mb-8">
-          <h2 class="text-xl font-bold mb-6">2. 面试记录</h2>
+          <h2 class="text-xl font-bold mb-6">3. 面试记录</h2>
           <div class="space-y-6">
             <div v-for="(record, index) in candidateInfo.interviewRecord" :key="index" class="border-b pb-4">
               <div class="mb-2">
@@ -774,9 +870,9 @@ const handleVideoError = (event: Event) => {
           </div>
         </div>
 
-        <!-- 视频记录 -->
+        <!-- 4. 视频记录 -->
         <div class="mb-8">
-          <h2 class="text-xl font-bold mb-6">3. 视频记录</h2>
+          <h2 class="text-xl font-bold mb-6">4. 视频记录</h2>
           <div class="space-y-8">
             <div v-for="(category, index) in candidateInfo.videoRecords" :key="index">
               <h3 class="text-lg font-semibold mb-4">{{ category.category }}</h3>
@@ -804,7 +900,7 @@ const handleVideoError = (event: Event) => {
 
         <!-- 其他信息 -->
         <div class="mb-8">
-          <h2 class="text-xl font-bold mb-6">4. 其他信息</h2>
+          <h2 class="text-xl font-bold mb-6">5. 其他信息</h2>
           <div class="space-y-6">
             <!-- 验证状态 -->
             <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
@@ -860,93 +956,40 @@ const handleVideoError = (event: Event) => {
             </div>
           </div>
         </div>
-
-        <!-- 评估表单 -->
-        <!-- <a-form
-          ref="formRef"
-          :model="candidateInfo"
-          layout="vertical"
-        >
-    
-          <div class="mb-8">
-            <h2 class="text-xl font-bold mb-4">6. 面试评分</h2>
-            <a-form-item
-              label="请为候选人打分"
-              name="score"
-              :rules="[{ required: true, message: '请选择评分' }]"
-            >
-              <a-rate
-                v-model:value="evaluationScore"
-                :count="5"
-                allow-half
-              />
-            </a-form-item>
-          </div>
-
-        
-          <div class="mb-8">
-            <h2 class="text-xl font-bold mb-4">评价意见</h2>
-            <a-form-item
-              label="请输入评价意见"
-              name="comments"
-              :rules="[{ required: true, message: '请输入评价意见' }]"
-            >
-              <a-textarea
-                v-model:value="evaluationComments"
-                :rows="4"
-                placeholder="请输入您的评价意见..."
-              />
-            </a-form-item>
-          </div>
-
-       
-          <div class="flex justify-end">
-            <a-button type="primary" @click="handleSubmit">
-              提交评估
-            </a-button>
-          </div>
-        </a-form> -->
       </div>
     </a-spin>
 
-    <!-- 悬浮按钮 -->
+    <!-- 悬浮按钮 (Element Plus 版本) -->
     <div class="fixed right-8 bottom-24 flex flex-col space-y-4">
-      <a-button
+      <el-button
         type="primary"
-        shape="circle"
-        class="flex items-center justify-center"
+        style="margin-left: 12px;"
+        circle
         @click="handleShare"
       >
-        <template #icon>
-          <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
-            <path d="M15 8a3 3 0 10-2.977-2.63l-4.94 2.47a3 3 0 100 4.319l4.94 2.47a3 3 0 10.895-1.789l-4.94-2.47a3.027 3.027 0 000-.74l4.94-2.47C13.456 7.68 14.19 8 15 8z" />
-          </svg>
-        </template>
-      </a-button>
-      <a-button
+        <el-icon><Share /></el-icon>
+      </el-button>
+      <el-button
         type="primary"
-        shape="circle"
-        class="flex items-center justify-center"
+        circle
         @click="handleDownload"
       >
-        <template #icon>
-          <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
-            <path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
-          </svg>
-        </template>
-      </a-button>
-      <a-button
+        <el-icon><Download /></el-icon>
+      </el-button>
+      <el-button
+        type="primary"
+        circle
+        @click="handlePrint"
+      >
+        <el-icon><Printer /></el-icon>
+      </el-button>
+      <el-button
         type="primary"
-        shape="circle"
-        class="flex items-center justify-center"
+        circle
         @click="scrollToTop"
       >
-        <template #icon>
-          <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
-            <path fill-rule="evenodd" d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z" clip-rule="evenodd" />
-          </svg>
-        </template>
-      </a-button>
+        <el-icon><ArrowUp /></el-icon>
+      </el-button>
     </div>
   </div>
 </template>
@@ -978,23 +1021,24 @@ const handleVideoError = (event: Event) => {
   border-radius: 50%;
 }
 
-/* 修改以下样式以确保页面可以滚动 */
+/* 修改滚动相关样式 */
 html, body {
   height: 100%;
-  min-height: 100%;
-  overflow-y: auto !important;
+  scroll-behavior: smooth;
 }
 
-/* 确保主容器不会限制滚动 */
+/* 确保主容器可以正常滚动 */
 .max-w-4xl {
   position: relative;
-  overflow: visible;
+  overflow-y: visible;
+  min-height: 100%;
 }
 
 /* 添加以下样式确保内容可以正常滚动 */
 body {
   margin: 0;
   padding: 0;
+  overflow-y: auto !important;
 }
 
 #app {
@@ -1003,6 +1047,14 @@ body {
   overflow: visible;
 }
 
+/* 确保滚动容器不会被阻止滚动 */
+.ant-layout, 
+.ant-layout-content,
+.el-scrollbar__wrap,
+.el-scrollbar__view {
+  overflow-y: auto !important;
+}
+
 @media (max-width: 768px) {
   .max-w-4xl {
     padding: 1rem;
@@ -1016,4 +1068,49 @@ body {
     right: 1rem;
   }
 }
+
+/* 打印样式 */
+@media print {
+  /* 打印时隐藏悬浮按钮 */
+  .fixed {
+    display: none !important;
+  }
+  
+  /* 确保内容完整显示 */
+  .max-w-4xl {
+    max-width: 100% !important;
+    padding: 0 !important;
+    margin: 0 !important;
+  }
+  
+  /* 调整页面边距 */
+  @page {
+    margin: 1cm;
+  }
+  
+  /* 确保背景色和图片打印 */
+  * {
+    -webkit-print-color-adjust: exact !important;
+    color-adjust: exact !important;
+    print-color-adjust: exact !important;
+  }
+  
+  /* 避免视频容器在打印时出现问题 */
+  .video-container {
+    page-break-inside: avoid;
+    break-inside: avoid;
+  }
+  
+  /* 确保每个主要部分在新页面开始 */
+  h2.text-xl {
+    page-break-before: always;
+    break-before: always;
+  }
+  
+  /* 第一个标题不需要分页 */
+  h2.text-xl:first-of-type {
+    page-break-before: avoid;
+    break-before: avoid;
+  }
+}
 </style>

+ 12 - 23
src/views/position/list/crud.tsx

@@ -158,36 +158,25 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 						value: 1,
 					},
 				},*/
-				status: {
-					title: '状态',
+				job_type_display: {
+					title: '职位类型',
 					search: { show: true },
 					type: 'dict-select',
 					column: {
 						width: 100,
-						/* component: {
-							name: 'fs-dict-label',
-							props: {
-								dict: dict({
-									data: [
-										{ value: 0, label: '草稿' },
-										{ value: 1, label: '已发布' },
-										{ value: 2, label: '已结束' }
-									]
-								})
-							}
-						}, */
 					},
 					dict: dict({
 						data: [
-							{ value: 0, label: '草稿' },
-							{ value: 1, label: '已发布' },
-							{ value: 2, label: '已结束' }
+							{ value: "全职", label: '全职' },
+							{ value: "兼职", label: '兼职' },
+							{ value: "实习", label: '实习' },
+							{ value: "其他", label: "其他" }
 						]
 					}),
 					form: {
-						rules: [{ required: true, message: '状态必填' }],
+						rules: [{ required: true, message: '职位类型必填' }],
 						component: {
-							placeholder: '请选择状态',
+							placeholder: '职位类型',
 						},
 					},
 				},
@@ -324,7 +313,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 					},
 				},
 				job_type: {
-					title: '职位类型',
+					title: '状态',
 					search: { show: true },
 					type: 'dict-select',
 					column: {
@@ -332,9 +321,9 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 					},
 					dict: dict({
 						data: [
-							{ value: 0, label: '全职' },
-							{ value: 1, label: '兼职' },
-							{ value: 2, label: '实习' }
+							{ value: 0, label: '未发布' },
+							{ value: 1, label: '已发布' },
+							{ value: 2, label: '已结束' }
 						]
 					}),
 					form: {

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

@@ -22,8 +22,8 @@ export function AddDocument(obj: AddReq) {
 /* 文档管理编辑 /api/system/document/{id}/*/
 export function UpdateDocument(obj: any) {
 	return request({
-		url: '/api/system/document/'+obj.id+ '/',
-		method: 'put',
+		url: 'api/system/interview_question/update_digital_human',
+		method: 'post',
 		data: obj,
 	});
 }

+ 39 - 29
src/views/questionBank/positionList/crud.tsx

@@ -4,7 +4,8 @@ import { dictionary } from '/@/utils/dictionary';
 import { successMessage } from '../../../utils/message';
 import { auth } from '/@/utils/authFunction';
 import { useRouter } from 'vue-router';
-import { ElMessage } from 'element-plus';
+import { ElMessage, ElDialog, ElButton, ElUpload } from 'element-plus';
+import { ref } from 'vue';
 
 export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
 	const router = useRouter(); // 添加这行来获取router实例
@@ -66,7 +67,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 				}
 			},
 			rowHandle: {
-				width: 250,
+				width: 300, // 增加宽度以容纳新按钮
 				buttons: {
 					view: {
 						size: 'small',
@@ -75,34 +76,16 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 							window.dispatchEvent(event);
 						}
 					},
-					/* preview: {
-						text: '预览',
+					uploadVideo: {
+						text: '上传视频/字幕',
 						type: 'success',
 						size: 'small',
-						click: (row) => {
-							// 检查文件路径是否存在
-							if (!row.row || !row.row.file_path) {
-								ElMessage.error('文件路径无效');
-								return;
-							}
-
-							const filePath = row.row.file_path;
-							// 获取文件扩展名
-							const fileType = row.row.file_type;
-
-							// 支持的文件类型列表
-							const supportedTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
-
-							if (!supportedTypes.includes(fileType)) {
-								ElMessage.warning(`暂不支持预览该文件类型: ${fileType}`);
-								return;
-							}
-
-							// 使用window.open在新窗口中打开预览页面
-							const previewUrl = `/#/preview?url=${encodeURIComponent(filePath)}&type=${fileType}`;
-							window.open(previewUrl, '_blank');
+						click: ({ row }: any) => {
+							// 触发自定义事件,让父组件处理弹窗显示
+							const event = new CustomEvent('openVideoUploadDialog', { detail: row });
+							window.dispatchEvent(event);
 						}
-					}, */
+					},
 					edit: {
 						type: 'primary',
 						size: 'small',
@@ -511,8 +494,35 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 						show:false
 					},
 				},
-
+				// 添加视频相关字段
+				video_url: {
+					title: '视频链接',
+					search: { show: false },
+					column: { 
+						show: true,
+						width: 120,
+						formatter: ({ row }: any) => {
+							return row.video_url ? '已上传' : '未上传';
+						}
+					},
+					form: { show: false },
+				},
+				subtitle_url: {
+					title: '字幕文件',
+					search: { show: false },
+					column: { 
+						show: true,
+						width: 120,
+						formatter: ({ row }: any) => {
+							return row.subtitle_url ? '已上传' : '未上传';
+						}
+					},
+					form: { show: false },
+				},
 			},
-		}
+		},
+		// 不需要导出这些状态
+		// videoUploadDialogVisible,
+		// currentRow
 	};
 }; 

+ 398 - 1
src/views/questionBank/positionList/index.vue

@@ -113,6 +113,93 @@
         </span>
       </template>
     </el-dialog> -->
+
+    <!-- 修改视频上传对话框为表单格式 -->
+    <el-dialog
+      v-model="videoUploadDialogVisible"
+      title="上传数字人视频及字幕"
+      width="600px"
+      destroy-on-close
+    >
+      <el-form 
+        ref="videoFormRef" 
+        :model="videoForm" 
+        :rules="videoFormRules" 
+        label-width="100px"
+        label-position="top"
+      >
+        <el-form-item label="数字人视频" prop="videoUrl">
+         <!--  <div class="upload-section"> -->
+            <el-upload
+              class="video-uploader"
+              :action="getBaseURL() + 'api/system/admin_upload/'"
+              :headers="{
+                Authorization: 'JWT ' + Session.get('token')
+              }"
+              :data="{
+                tenant_id:1
+              }"
+              :multiple="false"
+              :on-success="handleVideoUploadSuccess"
+              :on-error="handleUploadError"
+              :before-upload="beforeVideoUpload"
+              :show-file-list="false"
+              
+            ><!-- accept="video/*" -->
+              <div v-if="videoInfo.url" class="video-preview">
+                <video :src="videoInfo.url" controls class="video-player"></video>
+              </div>
+              <el-button v-else type="primary" icon="Plus">选择视频文件</el-button>
+              <div v-if="videoInfo.name" class="file-info">
+                <span>{{ videoInfo.name }}</span>
+                <span class="file-size">{{ formatFileSize(videoInfo.size) }}</span>
+              </div>
+            </el-upload>
+        <!--   </div> -->
+        </el-form-item>
+
+        <el-form-item label="字幕内容" prop="subtitleContent">
+          <!-- <div class="upload-section"> -->
+            <el-input
+              v-model="videoForm.subtitleContent"
+              type="textarea"
+              :rows="4"
+              placeholder="请输入数字人视频字幕内容"
+            ></el-input>
+           
+           <!--  <el-upload
+              class="subtitle-uploader mt-2"
+              :action="getBaseURL() + 'api/system/minioupload/'"
+              :headers="{
+                Authorization: 'JWT ' + Session.get('token')
+              }"
+              :multiple="false"
+              :on-success="handleSubtitleUploadSuccess"
+              :on-error="handleUploadError"
+              :before-upload="beforeSubtitleUpload"
+              :show-file-list="false"
+              accept=".srt,.vtt,.ass"
+            >
+              <div v-if="subtitleInfo.url" class="file-preview">
+                <el-icon class="file-icon"><Document /></el-icon>
+                <div class="file-info">
+                  <span>{{ subtitleInfo.name }}</span>
+                  <span class="file-size">{{ formatFileSize(subtitleInfo.size) }}</span>
+                </div>
+              </div>
+              <el-button v-else type="primary" icon="Plus">选择字幕文件</el-button>
+            </el-upload> -->
+        <!--   </div> -->
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="videoUploadDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitVideoForm" :disabled="!videoInfo.url">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
   </fs-page>
 </template>
 
@@ -128,7 +215,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 } from './api';
+import { GetDocumentTree, DeleteDocumentCategory, UpdateDocument } from './api';
 import { useRouter } from 'vue-router';
 /* import { print } from '/@/utils/print'; */
 
@@ -170,6 +257,44 @@ const detailData = ref({
 // 添加打印区域的ref
 const printArea = ref(null);
 
+// 视频和字幕上传相关状态
+const videoInfo = ref({
+  url: '',
+  name: '',
+  size: 0,
+  file_type: ''
+});
+
+const subtitleInfo = ref({
+  url: '',
+  name: '',
+  size: 0,
+  file_type: ''
+});
+
+// 视频上传对话框状态
+const videoUploadDialogVisible = ref(false);
+const currentRow = ref(null);
+
+// 添加字幕内容输入框
+const subtitleContent = ref('');
+
+// 添加表单引用
+const videoFormRef = ref(null);
+
+// 添加表单数据对象
+const videoForm = ref({
+  videoUrl: '',
+  subtitleContent: ''
+});
+
+// 添加表单验证规则
+const videoFormRules = {
+  videoUrl: [
+    { required: true, message: '请上传数字人视频', trigger: 'change' }
+  ]
+};
+
 // 获取文档树数据
 const getTreeData = () => {
   GetDocumentTree({}).then((ret: any) => {
@@ -420,6 +545,210 @@ const handlePrint = () => {
   }); */
 };
 
+// 视频上传前的验证
+const beforeVideoUpload = (file) => {
+  const isVideo = file.type.startsWith('video/');
+  const isLt500M = file.size / 1024 / 1024 < 500;
+
+  if (!isVideo) {
+    ElMessage.error('请上传视频文件!');
+    return false;
+  }
+  if (!isLt500M) {
+    ElMessage.error('视频大小不能超过 500MB!');
+    return false;
+  }
+  return true;
+};
+
+// 字幕上传前的验证
+const beforeSubtitleUpload = (file) => {
+  const validExtensions = ['.srt', '.vtt', '.ass'];
+  const extension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
+  const isValidType = validExtensions.includes(extension);
+  const isLt2M = file.size / 1024 / 1024 < 2;
+
+  if (!isValidType) {
+    ElMessage.error('请上传有效的字幕文件 (.srt, .vtt, .ass)!');
+    return false;
+  }
+  if (!isLt2M) {
+    ElMessage.error('字幕文件大小不能超过 2MB!');
+    return false;
+  }
+  return true;
+};
+
+// 修改视频上传成功处理函数
+const handleVideoUploadSuccess = (response, file) => {
+  console.log('response', response);
+  console.log('file', file);
+  
+  // 检查响应格式,确保能正确获取URL
+  if (response && response.url) {
+    // 直接使用响应中的URL
+    videoInfo.value = {
+      url: response.url,
+      name: file.name || response.filename || '',
+      size: file.size || 0,
+      file_type: file.type || response.mime_type || 'video/mp4'
+    };
+    
+    // 更新表单数据
+    videoForm.value.videoUrl = response.url;
+    ElMessage.success('视频上传成功');
+  } else if (response && response.data && response.data.url) {
+    // 兼容另一种可能的响应格式
+    videoInfo.value = {
+      url: response.data.url,
+      name: file.name || response.data.filename || '',
+      size: file.size || 0,
+      file_type: file.type || response.data.mime_type || 'video/mp4'
+    };
+    
+    // 更新表单数据
+    videoForm.value.videoUrl = response.data.url;
+    ElMessage.success('视频上传成功');
+  } else {
+    // 尝试从您提供的响应格式中获取URL
+    try {
+      // 根据您提供的响应示例,URL可能在这个位置
+      const url = response.url || 
+                 (response.response && response.response.url) || 
+                 (response[0] && response[0].url) ||
+                 (response.data && response.data.bucket_url);
+      
+      if (url) {
+        videoInfo.value = {
+          url: url,
+          name: file.name || response.filename || response.original_filename || '',
+          size: file.size || response.size || 0,
+          file_type: file.type || response.mime_type || 'video/mp4'
+        };
+        
+        // 更新表单数据
+        videoForm.value.videoUrl = url;
+        ElMessage.success('视频上传成功');
+      } else {
+        throw new Error('无法从响应中获取URL');
+      }
+    } catch (error) {
+      console.error('处理上传响应时出错:', error);
+      ElMessage.error('视频上传成功,但无法解析响应数据');
+    }
+  }
+};
+
+// 修改字幕上传成功处理函数
+const handleSubtitleUploadSuccess = (response, file) => {
+  if (response && response.data) {
+    subtitleInfo.value = {
+      url: response.data.bucket_url,
+      name: file.name,
+      size: file.size,
+      file_type: file.name.substring(file.name.lastIndexOf('.') + 1)
+    };
+    
+    // 尝试读取字幕文件内容
+    fetch(response.data.bucket_url)
+      .then(res => res.text())
+      .then(text => {
+        videoForm.value.subtitleContent = text;
+        ElMessage.success('字幕文件已上传并内容已加载');
+      })
+      .catch(err => {
+        console.error('无法读取字幕文件内容:', err);
+        ElMessage.warning('字幕文件已上传,但无法读取内容');
+      });
+  } else {
+    ElMessage.error('字幕上传失败');
+  }
+};
+
+// 上传错误处理
+const handleUploadError = () => {
+  ElMessage.error('上传失败,请重试');
+};
+
+// 处理打开视频上传对话框
+const handleOpenVideoUploadDialog = (event) => {
+  const row = event.detail;
+  if (row) {
+    currentRow.value = row;
+    videoUploadDialogVisible.value = true;
+    
+    // 重置上传状态
+    videoInfo.value = { url: '', name: '', size: 0, file_type: '' };
+    subtitleInfo.value = { url: '', name: '', size: 0, file_type: '' };
+    
+    // 重置表单数据
+    videoForm.value = {
+      videoUrl: '',
+      subtitleContent: ''
+    };
+  }
+};
+
+// 提交表单
+const submitVideoForm = () => {
+  if (!videoFormRef.value) return;
+  
+  videoFormRef.value.validate(async (valid) => {
+    if (valid) {
+      try {
+        if (!currentRow.value || !currentRow.value.id) {
+          ElMessage.error('当前选中的题目信息无效');
+          return;
+        }
+
+        // 确保我们有视频URL
+        if (!videoForm.value.videoUrl && !videoInfo.value.url) {
+          ElMessage.warning('请先上传数字人视频');
+          return;
+        }
+
+        // 修改为符合API要求的参数格式
+        const updateData = {
+          question_id: currentRow.value.question_id, // 必填,问题ID
+          video_url: videoForm.value.videoUrl || videoInfo.value.url, // 数字人视频URL
+          digital_human_video_subtitle: videoForm.value.subtitleContent, // 数字人视频字幕内容
+          tenant_id: 1
+        };
+
+        console.log('提交的数据:', updateData);
+        
+        const response = await UpdateDocument(updateData);
+        
+        if (response && response.code === 2000) {
+          ElMessage.success('数字人视频和字幕信息保存成功');
+          videoUploadDialogVisible.value = false;
+          
+          // 重置上传状态
+          videoInfo.value = { url: '', name: '', size: 0, file_type: '' };
+          subtitleInfo.value = { url: '', name: '', size: 0, file_type: '' };
+          
+          // 重置表单数据
+          videoForm.value = {
+            videoUrl: '',
+            subtitleContent: ''
+          };
+          
+          // 刷新列表
+          crudExpose.doRefresh();
+        } else {
+          ElMessage.error(response?.msg || '保存失败');
+        }
+      } catch (error) {
+        console.error('保存视频和字幕信息时出错:', error);
+        ElMessage.error('保存失败,请重试');
+      }
+    } else {
+      ElMessage.warning('请完成必填项');
+      return false;
+    }
+  });
+};
+
 // 页面加载时获取数据
 onMounted(() => {
   getTreeData();
@@ -449,11 +778,13 @@ onMounted(() => {
 
   // 添加全局事件监听器
   window.addEventListener('viewDocumentDetail', handleViewDetail);
+  window.addEventListener('openVideoUploadDialog', handleOpenVideoUploadDialog);
 });
 
 // 添加 onBeforeUnmount 钩子移除事件监听器
 onBeforeUnmount(() => {
   window.removeEventListener('viewDocumentDetail', handleViewDetail);
+  window.removeEventListener('openVideoUploadDialog', handleOpenVideoUploadDialog);
 });
 </script>
 
@@ -527,4 +858,70 @@ onBeforeUnmount(() => {
 .mt-4 {
   margin-top: 1rem;
 }
+::v-deep .el-upload{
+  flex-direction: column;
+}
+/* 表单样式优化 */
+.el-form-item {
+  margin-bottom: 20px;
+}
+
+.upload-section {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 15px;
+  background-color: #f8f9fa;
+}
+
+.video-uploader, .subtitle-uploader {
+  width: 100%;
+}
+
+.video-preview {
+  width: 100%;
+  margin-bottom: 10px;
+}
+
+.video-player {
+  width: 100%;
+  max-height: 200px;
+  object-fit: contain;
+  border-radius: 4px;
+}
+
+.file-preview {
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  border: 1px dashed #d9d9d9;
+  border-radius: 4px;
+  background-color: #fff;
+}
+
+.file-icon {
+  font-size: 24px;
+  margin-right: 10px;
+  color: #409eff;
+}
+
+.file-info {
+  display: flex;
+  flex-direction: column;
+  margin-top: 5px;
+}
+
+.file-size {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 3px;
+}
+
+.subtitle-tip {
+  color: #909399;
+  font-size: 12px;
+}
+
+.mt-2 {
+  margin-top: 8px;
+}
 </style>