|
|
@@ -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>
|
|
|
|