XieXing 4 месяцев назад
Родитель
Сommit
b38720840c

+ 42 - 0
@SETUP_INSTRUCTIONS.md

@@ -0,0 +1,42 @@
+# 前端BUG修复与功能调整说明
+
+## 问题描述与功能调整
+1. **BUG修复**: 在AI对话模块中,当报告输出到一半时用户切换到其他页面再切回来,会显示为空白记录。
+2. **BUG修复**: 移动端AI对话模块中,点击报告中的文件预览组件时,由于没有移动端预览组件会显示报错。
+3. **功能调整**: 暂时隐藏AI对话报告结果中的语音朗读图标。
+4. **BUG修复**: 移动端AI对话页面语音输入(STT)无法使用,提示"当前浏览器不支持语音识别"。
+
+## 修复与调整方案
+已修改 `shudao-vue-frontend/src/views/Chat.vue` 和 `shudao-vue-frontend/src/views/mobile/m-Chat.vue` 文件,实现以下改进:
+
+1. **保持流式输出状态**: 在 `onActivated`生命周期钩子中,保留正在输出的消息(`isTyping`为true),不再将其作为已完成消息处理。
+2. **显示输出内容**: 确保切换回来时,未完成的消息也能正常显示已输出的内容。
+3. **移动端PDF预览**: 在移动端引入了 `MobilePdfViewer` 组件,并在文件预览弹窗中针对 PDF 文件使用该组件进行渲染,解决了预览报错的问题。
+4. **隐藏语音朗读**: 使用 `v-if="false"` 隐藏了PC端和移动端AI对话报告结果中的语音朗读按钮。
+5. **修复语音输入**: 修正了移动端Chat页面语音输入功能的逻辑,移除了导致误判的浏览器支持性检查,现在可以正常启动语音识别。
+
+## 需要测试的功能
+1. **流式输出保持**:
+   - 开始一个AI对话,等待报告输出到一半
+   - 切换到其他功能页面(如:隐患识别、政策文档等)
+   - 切换回AI对话页面
+   - 验证:正在输出的报告内容应该保持显示,且仍在继续输出
+
+2. **移动端文件预览**:
+   - 在移动端AI对话中,生成包含PDF文件的报告
+   - 点击文件预览按钮
+   - 验证: PDF文件能够正常加载和显示,没有报错
+
+3. **语音朗读图标**:
+   - 检查PC端和移动端AI对话页面
+   - 验证: 报告结果下方的操作栏中不再显示语音朗读图标
+
+4. **移动端语音输入**:
+   - 在移动端AI对话页面点击输入框右侧的语音按钮
+   - 验证: 能够正常启动录音,并显示录音状态指示器
+
+## 注意事项
+- 本次修改仅涉及前端Vue组件
+- 不需要执行任何终端命令
+- 不需要重新编译或重启服务
+- 修改将在保存后立即生效(热更新)

+ 1 - 1
shudao-go-backend/conf/app.conf

@@ -46,7 +46,7 @@ heartbeat_api_url = "http://localhost:24000/api/health"
 # # 基础URL配置 - 手动切换
 # # 本地环境: https://172.16.29.101:22000
 # # 生产环境: https://aqai.shudaodsj.com:22000
-# base_url = "https://aqai.shudaodsj.com:22000"
+base_url = "https://aqai.shudaodsj.com:22000"
 
 # Token验证API配置
 # 生产环境:使用外部认证服务

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

@@ -312,7 +312,7 @@
                         <img src="@/assets/AIWriting/8.png" alt="删除" class="action-icon">
                         删除
                       </button>
-                      <button class="action-btn voice-btn" @click="handleVoiceRead(message)" :class="{ speaking: isSpeaking(message.id) }">
+                      <button v-if="false" class="action-btn voice-btn" @click="handleVoiceRead(message)" :class="{ speaking: isSpeaking(message.id) }">
                         <img :src="voiceIcon" alt="语音朗读" class="action-icon">
                         {{ isSpeaking(message.id) ? '停止朗读' : '语音朗读' }}
                       </button>
@@ -4555,13 +4555,28 @@ onActivated(async () => {
   
   // 重新渲染所有AI消息的markdown内容
   for (const message of chatMessages.value) {
-    if (message.type === 'ai' && message.content && !message.isTyping) {
+    if (message.type === 'ai' && message.content) {
       try {
-        console.log('重新渲染AI消息markdown:', message.id)
-        const processedReply = processAIResponse(message.content)
-        const processedReplyWithFileDisplay = processFileDisplay(processedReply, message.file)
-        const htmlReply = renderMarkdownContent(processedReplyWithFileDisplay)
-        message.displayContent = htmlReply
+        // 对于已完成的消息(!isTyping),重新渲染markdown
+        if (!message.isTyping) {
+          console.log('重新渲染已完成AI消息markdown:', message.id)
+          const processedReply = processAIResponse(message.content)
+          const processedReplyWithFileDisplay = processFileDisplay(processedReply, message.file)
+          const htmlReply = renderMarkdownContent(processedReplyWithFileDisplay)
+          message.displayContent = htmlReply
+        } else {
+          // 对于正在输出的消息(isTyping为true),保持其当前状态和显示内容
+          // 如果已有部分内容但displayContent为空,则重新渲染已输出的部分
+          if (message.content && (!message.displayContent || message.displayContent.trim() === '')) {
+            console.log('恢复正在输出的AI消息显示:', message.id, '已输出内容长度:', message.content.length)
+            const processedReply = processAIResponse(message.content)
+            const processedReplyWithFileDisplay = processFileDisplay(processedReply, message.file)
+            const htmlReply = renderMarkdownContent(processedReplyWithFileDisplay)
+            message.displayContent = htmlReply
+          } else {
+            console.log('保持正在输出的AI消息状态:', message.id, 'isTyping:', message.isTyping)
+          }
+        }
         
         // 重新绑定规范引用点击事件
         setTimeout(() => {
@@ -4577,6 +4592,10 @@ onActivated(async () => {
   // 强制触发Vue响应式更新
   chatMessages.value = [...chatMessages.value]
   
+  // 滚动到底部,确保用户能看到最新内容
+  await nextTick()
+  scrollToBottom()
+  
   console.log('页面重新激活完成,markdown内容已重新渲染')
 })
 

+ 0 - 7
shudao-vue-frontend/src/views/SafetyHazard.vue

@@ -444,14 +444,7 @@
 
                 <div class="outline-actions">
 
-                  <button class="action-btn exam-btn" @click="generateExam"
-                    :disabled="isGeneratingExam || !outlineData.length">
 
-                    <img src="@/assets/Safety/31.png" alt="考试" class="action-icon">
-
-                    生成考题
-
-                  </button>
 
                   <button class="action-btn" @click="copyEntireOutline" :disabled="isGeneratingOutline">
 

+ 74 - 15
shudao-vue-frontend/src/views/mobile/m-Chat.vue

@@ -235,7 +235,7 @@
                     <button class="action-btn regenerate-btn" @click="regenerateResponse(index)" :disabled="hasTypingMessage" title="重新生成">
                       <img :src="regenerateIcon" alt="重新生成" class="action-icon">
                     </button>
-                    <button class="action-btn voice-btn" @click="handleVoiceRead(message)" :title="isSpeaking(message.id) ? '停止朗读' : '语音朗读'" :class="{ speaking: isSpeaking(message.id) }">
+                    <button v-if="false" class="action-btn voice-btn" @click="handleVoiceRead(message)" :title="isSpeaking(message.id) ? '停止朗读' : '语音朗读'" :class="{ speaking: isSpeaking(message.id) }">
                       <img :src="voiceIcon" alt="语音朗读" class="action-icon">
                     </button>
                   </div>
@@ -434,6 +434,10 @@
             </svg>
             <p>{{ fileError }}</p>
           </div>
+          <MobilePdfViewer
+            v-else-if="previewFilePath && previewFileName.toLowerCase().endsWith('.pdf')"
+            :url="previewFilePath"
+          />
           <iframe
             v-else-if="previewFilePath"
             :src="previewFilePath"
@@ -468,6 +472,7 @@ import StreamMarkdown from '@/components/StreamMarkdown.vue'
 import WebSearchCapsule from '@/components/WebSearchCapsule.vue'
 import WebSearchSummary from '@/components/WebSearchSummary.vue'
 import StatusAvatar from '@/components/StatusAvatar.vue'
+import MobilePdfViewer from '@/components/MobilePdfViewer.vue'
 import { ref, onMounted, watch, nextTick, computed, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
 import { apis } from '@/request/apis.js'
 // ===== 已删除:getUserId - 不再需要,改用token =====
@@ -559,6 +564,36 @@ const previewFileName = ref('')
 const fileLoading = ref(false)
 const fileError = ref('')
 
+// 处理文件预览
+const handleFilePreview = (file) => {
+  console.log('预览文件:', file)
+  if (!file) return
+  
+  // 重置状态
+  fileLoading.value = true
+  fileError.value = ''
+  
+  // 设置文件信息
+  if (typeof file === 'string') {
+    previewFilePath.value = file
+    previewFileName.value = file.split('/').pop() || '未知文件'
+  } else {
+    previewFilePath.value = file.url || file.filePath || ''
+    previewFileName.value = file.name || file.fileName || '未知文件'
+  }
+  
+  // 显示弹窗
+  showFilePreview.value = true
+  
+  // 如果不是PDF(PDF由组件处理加载状态),iframe需要手动处理loading
+  if (!previewFileName.value.toLowerCase().endsWith('.pdf')) {
+    // iframe加载会在onload中设置fileLoading = false
+  } else {
+    // PDF组件自己处理loading,这里先设为false,让组件接管
+    fileLoading.value = false
+  }
+}
+
 // 消息内容引用
 const messageContentRefs = ref({})
 
@@ -4152,13 +4187,13 @@ const editUserMessage = (message) => {
 
 
 
+// 语音输入相关方法
 // 语音输入相关方法
 const handleVoiceClick = () => {
   console.log('点击语音按钮')
-  if (!speechSupported.value) {
-    showToastMessage('当前浏览器不支持语音识别')
-    return
-  }
+  // 移除对 speechSupported.value 的直接检查,让 startListening 内部去处理
+  // 因为 speechSupported 初始值为 false,且没有自动初始化,直接检查会导致误判
+  
   if (isListening.value) {
     // 如果正在录音,则停止
     stopVoiceInput()
@@ -4174,7 +4209,12 @@ const startVoiceInput = () => {
   // 开始语音识别
   const success = startListening()
   if (!success) {
-    showToastMessage('语音识别启动失败,请检查麦克风权限')
+    // 如果启动失败,优先显示内部错误信息
+    if (speechError.value) {
+      showToastMessage(speechError.value)
+    } else {
+      showToastMessage('语音识别启动失败,请检查麦克风权限')
+    }
   }
 }
 
@@ -4437,27 +4477,42 @@ onBeforeUnmount(() => {
 
 // 页面重新激活时,重新渲染所有AI消息的markdown内容
 onActivated(async () => {
-  console.log('移动端页面重新激活,检查并重新渲染markdown内容')
+  console.log('🔄 移动端页面重新激活,检查并重新渲染markdown内容')
   
   // 等待DOM更新
   await nextTick()
   
   // 重新渲染所有AI消息的markdown内容
   for (const message of chatMessages.value) {
-    if (message.type === 'ai' && message.content && !message.isTyping) {
+    if (message.type === 'ai' && message.content) {
       try {
-        console.log('重新渲染AI消息markdown:', message.id)
-        const processedReply = processAIResponse(message.content)
-        const processedReplyWithFileDisplay = processFileDisplay(processedReply, message.file)
-        const htmlReply = await renderWithVditor(processedReplyWithFileDisplay)
-        message.displayContent = htmlReply
+        // 对于已完成的消息(!isTyping),重新渲染markdown
+        if (!message.isTyping) {
+          console.log('重新渲染已完成AI消息markdown:', message.id)
+          const processedReply = processAIResponse(message.content)
+          const processedReplyWithFileDisplay = processFileDisplay(processedReply, message.file)
+          const htmlReply = await renderWithVditor(processedReplyWithFileDisplay)
+          message.displayContent = htmlReply
+        } else {
+          // 对于正在输出的消息(isTyping为true),保持其当前状态和显示内容
+          // 如果已有部分内容但displayContent为空,则重新渲染已输出的部分
+          if (message.content && (!message.displayContent || message.displayContent.trim() === '')) {
+            console.log('🔄 恢复正在输出的AI消息显示:', message.id, '已输出内容长度:', message.content.length)
+            const processedReply = processAIResponse(message.content)
+            const processedReplyWithFileDisplay = processFileDisplay(processedReply, message.file)
+            const htmlReply = await renderWithVditor(processedReplyWithFileDisplay)
+            message.displayContent = htmlReply
+          } else {
+            console.log('✅ 保持正在输出的AI消息状态:', message.id, 'isTyping:', message.isTyping)
+          }
+        }
         
         // 重新绑定规范引用点击事件
         setTimeout(() => {
           bindStandardReferenceEvents()
         }, 100)
       } catch (error) {
-        console.error('重新渲染markdown失败:', error)
+        console.error('重新渲染markdown失败:', error)
         // 如果重新渲染失败,保持原有内容
       }
     }
@@ -4466,7 +4521,11 @@ onActivated(async () => {
   // 强制触发Vue响应式更新
   chatMessages.value = [...chatMessages.value]
   
-  console.log('移动端页面重新激活完成,markdown内容已重新渲染')
+  // 滚动到底部,确保用户能看到最新内容
+  await nextTick()
+  scrollToBottom()
+  
+  console.log('✅ 移动端页面重新激活完成,markdown内容已重新渲染')
 })
 </script>
 

+ 0 - 8
shudao-vue-frontend/src/views/mobile/m-SafetyHazard.vue

@@ -203,18 +203,10 @@
           <div class="outline-header">
             <!-- 右上角:基础操作按钮 -->
             <div class="outline-top-right">
-              <button class="action-btn exam-btn" @click="generateExam" :disabled="isGeneratingExam || isGeneratingOutline || !aiOutlineContent">
-                <img :src="examIcon" alt="考试" class="action-icon">
-                生成考题
-              </button>
               <button class="action-btn" @click="copyEntireOutline" :disabled="isGeneratingOutline || isGeneratingExam">
                 <img :src="copyIcon" alt="复制" class="action-icon">
                 复制
               </button>
-              <button class="action-btn" @click="downloadOutlineAsWord" :disabled="isGeneratingOutline || isGeneratingExam">
-                <img :src="downloadIcon" alt="下载" class="action-icon">
-                下载
-              </button>
             </div>
           </div>