Kaynağa Gözat

考试工坊前后端调试

FanHong 3 gün önce
ebeveyn
işleme
2aa7c8184d

+ 10 - 61
shudao-chat-py/routers/chat.py

@@ -1,6 +1,5 @@
 from fastapi import APIRouter, Depends, Request
 from fastapi.responses import StreamingResponse, JSONResponse
-from sqlalchemy import or_
 from sqlalchemy.orm import Session
 from pydantic import BaseModel
 from typing import Optional
@@ -37,28 +36,6 @@ def _build_conversation_title(conversation: AIConversation) -> str:
     return _build_conversation_preview(conversation.content or "", limit=30)
 
 
-def _is_exam_workshop_conversation(conversation: Optional[AIConversation]) -> bool:
-    if not conversation:
-        return False
-    return conversation.business_type == 3 or bool((conversation.exam_name or "").strip())
-
-
-def _resolve_conversation_metadata(
-    conversation: Optional[AIConversation],
-    requested_business_type: int,
-    requested_exam_name: str,
-) -> tuple[int, str]:
-    requested_exam_name = (requested_exam_name or "").strip()
-
-    if _is_exam_workshop_conversation(conversation):
-        return 3, requested_exam_name or (conversation.exam_name or "").strip()
-
-    if requested_business_type == 3:
-        return 3, requested_exam_name
-
-    return requested_business_type, ""
-
-
 def _refresh_conversation_snapshot(db: Session, conversation_id: int, user_id: int) -> None:
     latest_message = (
         db.query(AIMessage)
@@ -200,31 +177,13 @@ async def send_deepseek_message(
 
         conversation_id = data.conversation_id or data.ai_conversation_id
 
-        existing_conversation = None
-        if conversation_id:
-            existing_conversation = (
-                db.query(AIConversation)
-                .filter(
-                    AIConversation.id == conversation_id,
-                    AIConversation.user_id == user.user_id,
-                    AIConversation.is_deleted == 0,
-                )
-                .first()
-            )
-
-        effective_business_type, effective_exam_name = _resolve_conversation_metadata(
-            existing_conversation,
-            data.business_type,
-            data.exam_name,
-        )
-
         # 创建或获取对话
         if not conversation_id:
             conversation = AIConversation(
                 user_id=user.user_id,
                 content=message[:100],
-                business_type=effective_business_type,
-                exam_name=effective_exam_name,
+                business_type=data.business_type,
+                exam_name=data.exam_name if data.business_type == 3 else "",
                 created_at=int(time.time()),
                 updated_at=int(time.time()),
                 is_deleted=0,
@@ -241,8 +200,8 @@ async def send_deepseek_message(
                 AIConversation.is_deleted == 0,
             ).update({
                 "content": message[:100],
-                "business_type": effective_business_type,
-                "exam_name": effective_exam_name,
+                "business_type": data.business_type,
+                "exam_name": data.exam_name if data.business_type == 3 else "",
                 "updated_at": int(time.time()),
             })
             db.commit()
@@ -386,10 +345,9 @@ async def send_deepseek_message(
                 _refresh_conversation_snapshot(db, conv_id, user.user_id)
                 db.commit()
 
-                if effective_exam_name:
+                if data.exam_name:
                     db.query(AIConversation).filter(AIConversation.id == conv_id).update(
-                        {"business_type": 3,
-                            "exam_name": effective_exam_name,
+                        {"exam_name": data.exam_name,
                             "updated_at": int(time.time())}
                     )
                     db.commit()
@@ -411,7 +369,7 @@ async def send_deepseek_message(
                 "content": response_text,
                 "message": response_text,
                 "user_id": user.user_id,
-                "business_type": effective_business_type,
+                "business_type": data.business_type,
             },
         }
     except Exception as e:
@@ -471,18 +429,9 @@ async def get_history_record(
     )
 
     if business_type is not None:
-        if business_type == 3:
-            conversations_query = conversations_query.filter(
-                or_(
-                    AIConversation.business_type == 3,
-                    AIConversation.exam_name.isnot(None),
-                    AIConversation.exam_name != "",
-                )
-            )
-        else:
-            conversations_query = conversations_query.filter(
-                AIConversation.business_type == business_type
-            )
+        conversations_query = conversations_query.filter(
+            AIConversation.business_type == business_type
+        )
 
     total = conversations_query.count()
     conversations = (

+ 7 - 5
shudao-vue-frontend/src/views/Chat.vue

@@ -2838,13 +2838,15 @@ const handleHistoryItem = async (historyItem) => {
     if (success) {
       chatMessages.value = chatMessages.value.filter(msg => msg.id !== loadingMessage.id)
       
-      // 根据历史记录的业务类型切换当前模式
-      if (historyItem.businessType === 2) {
+      // 根据历史记录的业务类型进行跳转或模式切换
+      const bType = Number(historyItem.businessType)
+      if (bType === 3) {
+        router.push({ path: '/exam-workshop', query: { historyId: historyItem.id } })
+        return
+      } else if (bType === 2) {
         currentMode.value = 'ai-writing'
-      } else if (historyItem.businessType === 1) {
+      } else if (bType === 1) {
         currentMode.value = 'safety-training'
-      } else if (historyItem.businessType === 3) {
-        currentMode.value = 'exam-workshop'
       } else {
         currentMode.value = 'ai-qa'
       }

+ 128 - 165
shudao-vue-frontend/src/views/ExamWorkshop.vue

@@ -69,13 +69,14 @@
         </div>
         <!-- 考试工坊主界面 -->
         <div v-if="!showExamDetail" class="exam-workshop-card app-container">
-            <!-- 中间主操作区 -->
-            <main class="main-content" style="padding-top: 36px;">
+                <!-- 中间主操作区 -->
+            <main class="main-content" style="padding-top: 36px; position: relative;">
+                <!-- 返回AI问答按钮 -->
+                <button v-if="!showExamDetail" class="return-ai-btn has-before" @click="handleReturnToAI">
+                  返回AI问答
+                </button>
+                
                 <div class="form-group" style="position: relative;">
-                    <!-- 返回AI问答按钮 -->
-                    <button v-if="hideSidebar && !showExamDetail" class="return-ai-btn" @click="handleReturnToAI">
-                      返回AI问答
-                    </button>
                     <label class="form-label">试卷名称</label>
                     <input type="text" class="form-control" v-model="examName" maxlength="32" placeholder="请输入试卷名称..." :disabled="isGenerating">
                     <div class="char-count">{{ examName?.length || 0 }}/32</div>
@@ -83,23 +84,27 @@
 
                 <div class="form-group">
                     <label class="form-label">出题依据内容</label>
-                    <textarea class="form-control" v-model="questionBasis" placeholder="在此输入知识点、章节或培训内容..." :disabled="isGenerating || selectedFile"></textarea>
+                    <textarea class="form-control" v-model="questionBasis" placeholder="在此输入知识点、章节或培训内容..." :disabled="isGenerating || uploadedFiles.length > 0"></textarea>
                     
-                    <div class="ppt-upload-section" @click="!isGenerating && !selectedFile ? triggerFileUpload() : null">
-                        <div class="ppt-upload-content">
-                            <div class="ppt-upload-icon-wrapper">
-                                <el-icon style="font-size: 28px; color: #4b5563;"><UploadFilled /></el-icon>
-                            </div>
-                            <div class="ppt-upload-text-wrapper">
-                                <div class="ppt-upload-title">从PPT生成考题</div>
-                                <div class="ppt-upload-hint">上传培训PPT,智能提取关键内容生成考题(单个文件可上传20M内)</div>
+                    <div class="ppt-upload-section" style="flex-direction: column; align-items: flex-start;" @click="!isGenerating ? triggerFileUpload() : null">
+                        <div style="display: flex; width: 100%; justify-content: space-between; align-items: center;">
+                            <div class="ppt-upload-content">
+                                <div class="ppt-upload-icon-wrapper">
+                                    <el-icon style="font-size: 28px; color: #4b5563;"><UploadFilled /></el-icon>
+                                </div>
+                                <div class="ppt-upload-text-wrapper">
+                                    <div class="ppt-upload-title">从PPT生成考题</div>
+                                    <div class="ppt-upload-hint">上传培训PPT,智能提取关键内容生成考题(支持多文件,单文件20M内)</div>
+                                </div>
                             </div>
+                            <el-icon class="ppt-arrow"><ArrowRight /></el-icon>
                         </div>
-                        <el-icon class="ppt-arrow"><ArrowRight /></el-icon>
                         
-                        <div v-if="selectedFile" class="file-status-badge" @click.stop>
-                          <span class="file-name truncate">已上传: {{ selectedFile.name }}</span>
-                          <span @click.stop="removeSelectedFile" class="remove-btn">×</span>
+                        <div v-if="uploadedFiles.length > 0" class="files-list" @click.stop style="width: 100%; display: flex; flex-wrap: wrap; gap: 8px;">
+                          <div v-for="(file, index) in uploadedFiles" :key="index" class="file-status-badge">
+                            <span class="file-name truncate">已上传: {{ file.name }}</span>
+                            <span @click.stop="removeSelectedFile(index)" class="remove-btn">×</span>
+                          </div>
                         </div>
                     </div>
                 </div>
@@ -209,12 +214,11 @@
           <!-- 详情页头部 -->
           <div class="detail-header">
             <div class="header-left">
-              <button class="back-btn" @click="backToConfig" :disabled="isGenerating">
-                <span class="back-arrow">←</span>
+            </div>
+            <div class="header-right" style="display: flex; align-items: center; gap: 12px;">
+              <button class="return-ai-btn has-before" style="position: static;" @click="backToConfig" :disabled="isGenerating">
                 返回修改
               </button>
-            </div>
-            <div class="header-right">
               <!-- <button class="save-btn" @click="saveExam" :disabled="isGenerating">
                 <img :src="saveIcon" alt="保存试卷" class="save-icon" />
               </button> -->
@@ -565,6 +569,7 @@
       ref="fileInput"
       type="file"
       accept=".ppt,.pptx"
+      multiple
       style="display: none"
       @change="handleFileSelect"
     />
@@ -583,6 +588,7 @@
 
 <script setup>
 import { ref, computed, onMounted, onUnmounted, reactive, watch, defineProps, defineEmits } from "vue";
+import { useRoute, useRouter } from "vue-router";
 import Sidebar from "@/components/Sidebar.vue";
 import DeleteConfirmModal from "@/components/DeleteConfirmModal.vue";
 import { UploadFilled, ArrowRight, Delete, MagicStick, Loading } from '@element-plus/icons-vue';
@@ -596,6 +602,9 @@ const props = defineProps({
 
 const emit = defineEmits(['return-to-ai']);
 
+const route = useRoute();
+const router = useRouter();
+
 const handleReturnToAI = () => {
   emit('return-to-ai');
 };
@@ -655,7 +664,7 @@ const editModalData = ref({
 
 // PPT文件上传相关
 const fileInput = ref(null);
-const selectedFile = ref(null);
+const uploadedFiles = ref([]);
 const isUploadingFile = ref(false);
 const fileContent = ref(''); // 存储文件内容
 const pptContentDescription = ref(''); // 存储用户输入的PPT内容描述
@@ -693,22 +702,6 @@ const currentExam = ref(null);
 const historyData = ref([])
 const historyTotal = ref(0) // 历史记录总数
 
-
-const isExamWorkshopConversation = (conversation = {}) => {
-  const content = String(conversation.content || '')
-  const title = String(conversation.title || '')
-  const examName = String(conversation.exam_name || '')
-
-  return (
-    Number(conversation.business_type) === 3 ||
-    !!examName.trim() ||
-    title.includes('技术考核') ||
-    content.includes('请根据以下要求直接生成一份完整试卷') ||
-    content.includes('"singleChoice"') ||
-    content.includes('"totalQuestions"')
-  )
-}
-
 // 获取历史记录列表
 const getHistoryRecordList = async () => {
   try {
@@ -717,9 +710,8 @@ const getHistoryRecordList = async () => {
     const startTime = performance.now()
     
     const response = await apis.getHistoryRecord({ 
-      // ===== 已删除:user_id - 后端从token解析 =====
-      ai_conversation_id: 0, // 0表示获取对话列表
-      business_type: 3 // 考试工坊类型
+      ai_conversation_id: 0,
+      business_type: 3
     })
     
     const endTime = performance.now()
@@ -727,46 +719,19 @@ const getHistoryRecordList = async () => {
     console.log('📋 考试工坊历史记录列表响应:', response)
     
     if (response.statusCode === 200) {
-      const directConversations = Array.isArray(response.data) ? response.data : []
-      let conversations = [...directConversations]
-
-      const fallbackResponse = await apis.getHistoryRecord({
-        ai_conversation_id: 0
-      })
-
-      if (fallbackResponse.statusCode === 200 && Array.isArray(fallbackResponse.data)) {
-        const inferredExamConversations = fallbackResponse.data.filter(isExamWorkshopConversation)
-        const conversationMap = new Map()
-
-        directConversations.concat(inferredExamConversations).forEach((conversation) => {
-          if (!conversation?.id) return
-          conversationMap.set(conversation.id, conversation)
-        })
-
-        conversations = Array.from(conversationMap.values()).sort((a, b) => {
-          return Number(b.updated_at || 0) - Number(a.updated_at || 0)
-        })
-      }
-
-      // 设置历史记录总数
-      historyTotal.value = conversations.length
-      
-      // 转换后端数据为前端格式
-      historyData.value = conversations.map(conversation => ({
+      historyTotal.value = response.total || 0
+      historyData.value = response.data.map(conversation => ({
         id: conversation.id,
-        title: generateConversationTitle(conversation.exam_name || conversation.title || conversation.content),
+        title: generateConversationTitle(conversation.exam_name),
         time: formatTime(conversation.updated_at),
         businessType: conversation.business_type,
         isActive: false,
-        // 保存原始数据用于后续查询
         rawData: conversation
       }))
       console.log(`✅ 考试工坊历史记录列表已设置: ${historyData.value.length}条记录,总数: ${historyTotal.value}`)
     } else {
       console.error('❌ 获取考试工坊历史记录列表失败:', response.statusCode)
     }
-  } catch (error) {
-    console.error('❌ 获取考试工坊历史记录列表失败:', error)
   } finally {
     isLoadingHistory.value = false
   }
@@ -902,7 +867,7 @@ const confirmDeleteHistory = async () => {
     
     if (response.statusCode === 200) {
       // 删除成功,从列表中移除
-      historyData.value.splice(index, 1)
+      removeExamWorkshopHistory(historyItem.id)
       
       // 如果删除的是当前激活的历史记录,需要清空界面并调用新建任务
       if (historyItem.isActive) {
@@ -980,7 +945,7 @@ const createNewChat = async () => {
   isRefreshing.value = {};
   
   // 清理文件
-  selectedFile.value = null;
+  uploadedFiles.value = [];
   pptContentDescription.value = '';
   
   // 清除所有历史记录的选中状态
@@ -1000,25 +965,20 @@ const handleHistoryItem = async (historyItem) => {
   isLoadingHistoryItem.value = true;
   
   try {
-    // 设置当前点击的历史记录为激活状态
     historyData.value.forEach((item) => {
       item.isActive = item.id === historyItem.id;
     });
     
-    // 获取该历史记录的详细内容
     const response = await apis.getHistoryRecord({ 
-      // ===== 已删除:user_id - 后端从token解析 =====
-      ai_conversation_id: historyItem.id, // 使用历史记录的ID作为ai_conversation_id
-      business_type: 3 // 考试工坊类型
+      ai_conversation_id: historyItem.id,
+      business_type: 3
     });
     console.log(response.data)
     if (response.statusCode === 200 && response.data && response.data.length > 0) {
-      // 获取最新的试卷数据(取最新的AI消息)
-      const latestRecord = response.data[response.data.length - 1]; // 获取最新记录
+      const latestRecord = response.data[response.data.length - 1];
       console.log('获取到的试卷数据:', latestRecord);
       console.log('试卷数据结构:', JSON.stringify(latestRecord, null, 2));
       currentTime.value = formatTime(latestRecord.created_at)
-      // 解析试卷数据并恢复
       if (latestRecord && latestRecord.content) {
         try {
           const examData = extractExamDataFromContent(latestRecord.content);
@@ -1026,24 +986,20 @@ const handleHistoryItem = async (historyItem) => {
           showExamDetail.value = true;
         } catch (error) {
           console.error('解析试卷数据失败:', error);
-          // 如果解析失败,显示默认详情页
           showExamDetail.value = true;
           currentTime.value = historyItem.time;
         }
       } else {
-        // 如果没有内容,显示默认详情页
         showExamDetail.value = true;
         currentTime.value = historyItem.time;
       }
     } else {
       console.error('获取历史记录详情失败:', response);
-      // 显示默认详情页
       showExamDetail.value = true;
       currentTime.value = historyItem.time;
     }
   } catch (error) {
     console.error('获取历史记录详情失败:', error);
-    // 显示默认详情页
     showExamDetail.value = true;
     currentTime.value = historyItem.time;
   } finally {
@@ -1183,7 +1139,7 @@ const generateExam = async () => {
     await generateAIExam();
   } else {
     // PPT生成方式
-    if (!selectedFile.value) {
+    if (uploadedFiles.value.length === 0) {
       ElMessage.warning("请先上传PPT文件");
       return;
     }
@@ -1229,17 +1185,14 @@ const generatePPTExam = async () => {
       showExamDetail.value = true;
       ElMessage.success("PPT试卷生成完成!");
       
-      // AI回复完成后,获取最新的历史记录
       await getHistoryRecordList();
       
-      // 如果是新对话,将最新的历史记录设为激活状态
       if (ai_conversation_id.value > 0) {
         historyData.value.forEach((item) => {
           item.isActive = item.id === ai_conversation_id.value;
         });
         console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
       } else {
-        // 如果没有对话ID,选中第一条记录
         selectLatestHistoryRecord();
       }
     } else {
@@ -1296,17 +1249,14 @@ const generateAIExam = async () => {
       showExamDetail.value = true;
       ElMessage.success("AI试卷生成完成!");
       
-      // AI回复完成后,获取最新的历史记录
       await getHistoryRecordList();
       
-      // 如果是新对话,将最新的历史记录设为激活状态
       if (ai_conversation_id.value > 0) {
         historyData.value.forEach((item) => {
           item.isActive = item.id === ai_conversation_id.value;
         });
         console.log('设置最新历史记录为激活状态,conversationId:', ai_conversation_id.value);
       } else {
-        // 如果没有对话ID,选中第一条记录
         selectLatestHistoryRecord();
       }
     } else {
@@ -1334,6 +1284,9 @@ const fetchExamPrompt = async (mode = 'ai') => {
     scorePerQuestion: Number(type.scorePerQuestion) || 0,
   }));
 
+  const pptContents = uploadedFiles.value.map(file => file.content).join('\n\n');
+  const finalContentBasis = pptContents || questionBasis.value || '';
+
   const payload = {
     mode,
     client: 'pc',
@@ -1341,7 +1294,7 @@ const fetchExamPrompt = async (mode = 'ai') => {
     examTitle: examName.value,
     totalScore: totalScore.value,
     questionTypes: normalizedQuestionTypes,
-    pptContent: selectedFile.value?.content || questionBasis.value || ''
+    pptContent: finalContentBasis
   };
 
   try {
@@ -2893,16 +2846,11 @@ const saveExam = async () => {
     
     console.log('准备保存的试卷数据:', examData);
     
-    // 调用后端保存接口
     const response = await apis.saveExam(examData);
     
     if (response.statusCode === 200) {
       ElMessage.success("试卷保存成功!");
-      
-      // 更新历史记录
       updateHistoryData(examData);
-      
-      // 可以在这里刷新历史记录列表
       console.log('试卷已保存到历史记录');
     } else {
       throw new Error('保存失败');
@@ -2987,17 +2935,15 @@ const prepareExamDataForSave = () => {
 // 更新历史记录数据
 const updateHistoryData = (examData) => {
   const newHistoryItem = {
-    id: Date.now(), // 使用时间戳作为临时ID
+    id: Date.now(),
     title: examData.exam_name,
     time: examData.generation_time,
     isActive: false,
-    examData: examData // 保存完整的试卷数据
+    examData: examData
   };
   
-  // 添加到历史记录开头
   historyData.value.unshift(newHistoryItem);
   
-  // 限制历史记录数量(比如最多保存20条)
   if (historyData.value.length > 20) {
     historyData.value = historyData.value.slice(0, 20);
   }
@@ -3449,65 +3395,68 @@ PPT文件处理失败,请手动描述PPT的主要内容、关键知识点、
 
 // 处理文件选择
 const handleFileSelect = async (event) => {
-  const file = event.target.files[0]
-  if (!file) return
+  const files = Array.from(event.target.files)
+  if (!files || files.length === 0) return
   
-  try {
-    // 验证文件
-    const fileExtension = validateFile(file)
-    
-    isUploadingFile.value = true
-    console.log('开始读取文件内容:', file.name)
-    
-    // 处理PPT文档
-    const extractedContent = await readPPTFile(file)
-    
-    // 创建文件信息对象
-    selectedFile.value = {
-      file,
-      name: file.name,
-      size: file.size,
-      type: fileExtension,
-      icon: getFileIcon(fileExtension),
-      content: extractedContent // 存储提取的内容
+  isUploadingFile.value = true
+  let successCount = 0;
+  
+  for (const file of files) {
+    try {
+      // 验证文件
+      const fileExtension = validateFile(file)
+      console.log('开始读取文件内容:', file.name)
+      
+      // 处理PPT文档
+      const extractedContent = await readPPTFile(file)
+      
+      // 创建文件信息对象
+      uploadedFiles.value.push({
+        file,
+        name: file.name,
+        size: file.size,
+        type: fileExtension,
+        icon: getFileIcon(fileExtension),
+        content: extractedContent // 存储提取的内容
+      })
+      successCount++;
+      
+      // 如果是第一个上传的文件,且当前试卷名称还是默认状态或为空,使用该文件名作为试卷名称
+      if (uploadedFiles.value.length === 1 && (!examName.value || examName.value.includes('工程施工技术考核'))) {
+        const fileNameWithoutExt = file.name.replace(/\.(ppt|pptx)$/i, '')
+        examName.value = `${fileNameWithoutExt}考试试卷`
+      }
+      
+    } catch (error) {
+      console.error(`文件 ${file.name} 读取失败:`, error)
+      ElMessage.error(`${file.name}读取失败: ${error.message || '请重试'}`)
     }
-    
-    // 使用文件名作为试卷名称(去掉扩展名)
-    const fileNameWithoutExt = file.name.replace(/\.(ppt|pptx)$/i, '')
-    examName.value = `${fileNameWithoutExt}考试试卷`
-    
-    // 显示提取的内容长度
-    const contentLength = extractedContent.length
-    console.log('文件内容提取完成,字符数:', contentLength)
-    ElMessage.success(`PPT文件读取成功,提取了${contentLength}个字符的内容`)
-    
-  } catch (error) {
-    console.error('文件读取失败:', error)
-    ElMessage.error(error.message || '文件读取失败,请重试')
-  } finally {
-    isUploadingFile.value = false
-    event.target.value = ''
   }
+  
+  if (successCount > 0) {
+    ElMessage.success(`成功读取了 ${successCount} 个文件`)
+  }
+  
+  isUploadingFile.value = false
+  event.target.value = ''
 }
 
 // 删除选中的文件
-const removeSelectedFile = () => {
-  if (selectedFile.value) {
-    selectedFile.value = null
-    // 清空PPT内容描述
-    pptContentDescription.value = ''
-    // 重置试卷名称为默认值
-    const projectTypeName = projectTypes[selectedProjectType.value].name
-    examName.value = `${projectTypeName}工程施工技术考核`
+const removeSelectedFile = (index) => {
+  if (index >= 0 && index < uploadedFiles.value.length) {
+    uploadedFiles.value.splice(index, 1)
+    
+    // 如果全部删除了,重置相关状态
+    if (uploadedFiles.value.length === 0) {
+      pptContentDescription.value = ''
+      const projectTypeName = projectTypes[selectedProjectType.value]?.name || '桥梁'
+      examName.value = `${projectTypeName}工程施工技术考核`
+    }
   }
 }
 
 // 触发文件上传
 const triggerFileUpload = () => {
-  if (selectedFile.value) {
-    ElMessage.warning('只能上传一个文件,请先删除当前文件')
-    return
-  }
   fileInput.value?.click()
 }
 
@@ -3538,6 +3487,15 @@ onMounted(async () => {
   // 获取历史记录列表
   await getHistoryRecordList()
   
+  // 检查URL参数是否有historyId需要加载
+  const historyId = route.query.historyId
+  if (historyId) {
+    const targetItem = historyData.value.find(item => String(item.id) === String(historyId))
+    if (targetItem) {
+      await handleHistoryItem(targetItem)
+    }
+  }
+  
   // 添加全局点击事件监听器
   document.addEventListener('click', handleClickOutside);
   
@@ -3731,11 +3689,11 @@ onUnmounted(() => {
 /* 工作头部 */
 .work-header {
   background: transparent;
-  padding: 30px 0px 0px 18px;
+  padding: 40px 0px 0px 18px;
   
   h2 {
     margin: 0;
-    font-size: 20px;
+    font-size: 25px;
     font-weight: 600;
     color: #2c3e50;
   }
@@ -3910,19 +3868,17 @@ onUnmounted(() => {
     }
 
     .file-status-badge {
-        position: absolute;
-        bottom: -40px;
-        left: 0;
         background: #ebf3ff;
         color: var(--primary-color);
-        padding: 8px 16px;
+        padding: 5px 12px;
         border-radius: 8px;
-        font-size: 13px;
+        font-size: 10px;
         display: flex;
         align-items: center;
-        gap: 12px;
+        gap: 8px;
         border: 1px solid rgba(13, 110, 253, 0.1);
-        max-width: 300px;
+        max-width: 100%;
+        margin-top: 12px;
     }
 
     .file-name {
@@ -5807,8 +5763,8 @@ onUnmounted(() => {
 
 .return-ai-btn {
   position: absolute;
-  top: -15px;
-  right: 0;
+  top: 10px;
+  right: 20px;
   z-index: 100;
   background: white;
   border: 1px solid rgba(0, 0, 0, 0.06);
@@ -5823,15 +5779,22 @@ onUnmounted(() => {
   align-items: center;
   gap: 5px;
   transition: all 0.3s ease;
+  height: 36px;
+  box-sizing: border-box;
+}
+
+.return-ai-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
 }
 
-.return-ai-btn:hover {
+.return-ai-btn:hover:not(:disabled) {
   box-shadow: 0 8px 24px rgba(13, 110, 253, 0.12);
   color: #0d6efd;
   border-color: rgba(13, 110, 253, 0.2);
 }
 
-.return-ai-btn::before {
+.return-ai-btn.has-before::before {
   content: '←';
   font-size: 16px;
   font-weight: bold;

+ 9 - 0
shudao-vue-frontend/src/views/mobile/m-Chat.vue

@@ -1496,6 +1496,15 @@ const handleHistoryItem = async (historyItem) => {
   
   // 关闭历史记录弹窗(移动端特有)
   showHistory.value = false
+
+  // 根据业务类型跳转到对应模块
+  const bType = Number(historyItem.businessType)
+  if (bType === 3) {
+    router.push({ path: '/mobile/exam-workshop', query: { historyId: historyItem.id } })
+    return
+  } else if (bType !== 0) {
+    // 其他非0业务类型也可以在这里做重定向或提示,但目前仅处理考试工坊
+  }
   
   // 清空当前消息
     chatMessages.value = []

+ 20 - 63
shudao-vue-frontend/src/views/mobile/m-ExamWorkshop.vue

@@ -481,7 +481,7 @@
 </template>
 
 <script setup>
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 import MobileHeader from '@/components/MobileHeader.vue'
 import MobileHistoryDrawer from '@/components/MobileHistoryDrawer.vue'
 import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
@@ -492,6 +492,7 @@ import { initNativeNavForSubPage } from '@/utils/nativeBridge.js'
 // import { getUserId } from '@/utils/userManager.js'
 
 const router = useRouter()
+const route = useRoute()
 
 const goBack = () => {
   router.go(-1)
@@ -776,25 +777,21 @@ const handleHistoryItem = async (historyItem) => {
 const deleteHistoryItem = async (historyItem, index) => {
   try {
     console.log('开始删除移动端历史记录:', historyItem)
-    
+
     const response = await apis.deleteHistoryRecord({
-      // ===== 已删除:user_id - 后端从token解析 =====
       ai_conversation_id: historyItem.id
     })
     
     if (response.statusCode === 200) {
-      // 从本地数据中移除
       historyData.value.splice(index, 1)
       historyTotal.value = Math.max(0, historyTotal.value - 1)
       
-      // 如果删除的是当前激活的历史记录,执行新建任务
       if (historyItem.isActive) {
         console.log('删除激活的历史记录,执行新建任务')
         createNewTask()
       }
       
       console.log('✅ 移动端历史记录删除成功')
-      // 轻提示
       showToast('删除成功')
     } else {
       console.error('❌ 删除移动端历史记录失败:', response)
@@ -840,22 +837,6 @@ const formatHistoryTime = (timestamp) => {
   return `${month}月${day}日 ${time}`
 }
 
-// 获取历史记录列表
-const isExamWorkshopConversation = (conversation = {}) => {
-  const content = String(conversation.content || '')
-  const title = String(conversation.title || '')
-  const examName = String(conversation.exam_name || '')
-
-  return (
-    Number(conversation.business_type) === 3 ||
-    !!examName.trim() ||
-    title.includes('技术考核') ||
-    content.includes('请根据以下要求直接生成一份完整试卷') ||
-    content.includes('"singleChoice"') ||
-    content.includes('"totalQuestions"')
-  )
-}
-
 const getHistoryRecordList = async () => {
   try {
     console.log('📋 开始获取移动端考试工坊历史记录列表...')
@@ -863,9 +844,8 @@ const getHistoryRecordList = async () => {
     const startTime = performance.now()
     
     const response = await apis.getHistoryRecord({ 
-      // ===== 已删除:user_id - 后端从token解析 =====
-      ai_conversation_id: 0, // 0表示获取对话列表
-      business_type: 3 // 考试工坊类型
+      ai_conversation_id: 0,
+      business_type: 3
     })
     
     const endTime = performance.now()
@@ -873,41 +853,16 @@ const getHistoryRecordList = async () => {
     console.log('📋 移动端历史记录列表响应:', response)
     
     if (response.statusCode === 200) {
-      const directConversations = Array.isArray(response.data) ? response.data : []
-      let conversations = [...directConversations]
-
-      const fallbackResponse = await apis.getHistoryRecord({
-        ai_conversation_id: 0
-      })
-
-      if (fallbackResponse.statusCode === 200 && Array.isArray(fallbackResponse.data)) {
-        const inferredExamConversations = fallbackResponse.data.filter(isExamWorkshopConversation)
-        const conversationMap = new Map()
-
-        directConversations.concat(inferredExamConversations).forEach((conversation) => {
-          if (!conversation?.id) return
-          conversationMap.set(conversation.id, conversation)
-        })
-
-        conversations = Array.from(conversationMap.values()).sort((a, b) => {
-          return Number(b.updated_at || 0) - Number(a.updated_at || 0)
-        })
-      }
-
-      // 设置历史记录总数
-      historyTotal.value = conversations.length
+      historyTotal.value = response.total || 0
       
-      // 转换后端数据为前端格式
-      historyData.value = conversations.map(conversation => ({
+      historyData.value = response.data.map(conversation => ({
         id: conversation.id,
-        title: generateConversationTitle(conversation.exam_name || conversation.title || conversation.content),
+        title: generateConversationTitle(conversation.content),
         time: formatHistoryTime(conversation.updated_at),
         businessType: conversation.business_type,
         isActive: false,
-        // 保存原始数据用于后续查询
         rawData: conversation
       }))
-      // 高亮当前对话
       if (ai_conversation_id.value) {
         historyData.value.forEach(item => { item.isActive = item.id === ai_conversation_id.value })
       }
@@ -915,8 +870,6 @@ const getHistoryRecordList = async () => {
     } else {
       console.error('❌ 获取移动端历史记录列表失败:', response.statusCode)
     }
-  } catch (error) {
-    console.error('❌ 获取移动端历史记录列表失败:', error)
   } finally {
     isLoadingHistory.value = false
   }
@@ -1095,14 +1048,6 @@ const generateExam = async () => {
       // 显示考试详情页
       showExamDetail.value = true;
 
-      // 刷新历史记录,确保新生成的试卷详情可被立即查看
-      await getHistoryRecordList();
-      if (ai_conversation_id.value > 0) {
-        historyData.value.forEach((item) => {
-          item.isActive = item.id === ai_conversation_id.value;
-        });
-      }
-
       console.log('✅ 移动端试卷生成完成!');
 
     } else {
@@ -1361,6 +1306,7 @@ const restoreExamFromHistory = (examData) => {
 
   examName.value = normalizedExam.title || examName.value
   totalScore.value = normalizedExam.totalScore || totalScore.value
+  currentTime.value = exam.generation_time || currentTime.value
 
   questionTypes.value = [
     { name: "单选题", scorePerQuestion: normalizedExam.singleChoice.scorePerQuestion, questionCount: normalizedExam.singleChoice.count, romanNumeral: "一" },
@@ -2270,6 +2216,17 @@ onMounted(async () => {
     
     // 初始化原生导航栏(子页面模式:返回按钮执行路由后退)
     initNativeNavForSubPage(() => router.back())
+    
+    // 检查URL参数是否有historyId需要加载
+    const historyId = route.query.historyId
+    if (historyId) {
+      // 需要先加载历史记录列表才能找到对应项
+      await getHistoryRecordList()
+      const targetItem = historyData.value.find(item => String(item.id) === String(historyId))
+      if (targetItem) {
+        await handleHistoryItem(targetItem)
+      }
+    }
   } catch (error) {
     console.error('❌ 移动端考试工坊页面初始化失败:', error)
   }