优化处理

This commit is contained in:
2025-10-26 16:59:50 +08:00
parent fdac026720
commit 2e243c7671
45 changed files with 346 additions and 3757 deletions
+20 -19
View File
@@ -52,7 +52,8 @@ export class ChatApiService {
async createSession(userId: string, title?: string): Promise<ChatSession> {
try {
console.log('📝 创建会话API调用:', { userId, title })
const response = await http.post<ConversationResponse>('/conversation', {
// backend-single: POST /conversation/create
const response = await http.post<ConversationResponse>('/conversation/create', {
userId,
title: title || `对话${Date.now()}`
})
@@ -88,25 +89,23 @@ export class ChatApiService {
async getUserSessions(userId: string): Promise<ChatSession[]> {
try {
console.log('📂 获取用户会话API调用:', { userId })
const response = await http.get<ConversationResponse[]>(`/conversation/user/${userId}`)
// backend-single: GET /conversation/page?userId=xxx
const response = await http.get<any>('/conversation/page', { params: { userId, current: 1, size: 100 } })
console.log('📂 获取用户会话API响应:', response)
// 处理HTTP响应的data字段
const data = (response as any).data || response
// 处理HTTP响应的data字段PageResult
const pageData = (response as any).data || response
const records = pageData.records || []
// 后端返回ConversationResponse数组,需要转换为ChatSession格式
if (Array.isArray(data)) {
return data.map((conv: any) => ({
id: conv.id,
title: conv.title,
userId: conv.userId || conv.user_id, // 兼容不同的字段名
createTime: conv.createTime || conv.create_time,
updateTime: conv.updateTime || conv.update_time,
messageCount: conv.messageCount || conv.message_count || 0
}))
}
return []
// 转换为ChatSession数组
return records.map((conv: any) => ({
id: conv.id,
title: conv.title,
userId: conv.userId || conv.user_id,
createTime: conv.createTime || conv.create_time,
updateTime: conv.updateTime || conv.update_time,
messageCount: conv.messageCount || conv.message_count || 0
}))
} catch (error) {
console.error('❌ 获取用户会话失败:', error)
return []
@@ -133,7 +132,8 @@ export class ChatApiService {
async deleteSession(sessionId: string): Promise<void> {
try {
console.log('🗑️ 删除会话API调用:', { sessionId })
await http.delete(`/conversation/${sessionId}`)
// backend-single: DELETE /conversation/delete?id=xxx
await http.delete('/conversation/delete', { params: { id: sessionId } })
console.log('✅ 删除会话成功')
} catch (error) {
console.error('❌ 删除会话失败:', error)
@@ -147,7 +147,8 @@ export class ChatApiService {
async updateSessionTitle(sessionId: string, title: string): Promise<void> {
try {
console.log('✏️ 更新会话标题API调用:', { sessionId, title })
await http.put(`/conversation/${sessionId}`, { title })
// backend-single: PUT /conversation/update 传id和title
await http.put('/conversation/update', { id: sessionId, title })
console.log('✅ 更新会话标题成功')
} catch (error) {
console.error('❌ 更新会话标题失败:', error)
+18 -9
View File
@@ -57,7 +57,8 @@ export const messageApi = {
// 获取用户消息分页
getUserMessages: async (current: number = 1, size: number = 20) => {
console.log('📨 调用getUserMessages API:', { current, size })
const response = await http.get(`/message/user/page`, { params: { current, size } })
// backend-single: GET /message/page (后端根据token识别用户)
const response = await http.get(`/message/page`, { params: { current, size } })
console.log('📨 getUserMessages API响应:', response)
return response
},
@@ -65,23 +66,31 @@ export const messageApi = {
// 搜索用户消息
searchUserMessages: async (keyword: string, limit: number = 50) => {
console.log('🔍 调用searchUserMessages API:', { keyword, limit })
const response = await http.post(`/message/user/search`, { keyword, limit })
console.log('🔍 searchUserMessages API响应:', response)
return response
// backend-single: POST /message/search
const resp = await http.post(`/message/search`, { keyword, limit })
console.log('🔍 searchUserMessages API响应:', resp)
// 统一返回数组,兼容控制器返回 PageResult 结构
const data: any = (resp as any).data || resp
const records = data.records || data
return Array.isArray(records) ? records : []
},
// 获取用户最近的聊天记录 - 修复:使用POST请求匹配后端接口
// 获取用户最近的聊天记录 - 返回数组,兼容后端 PageResult 结构
getRecentMessages: async (limit: number = 10) => {
console.log('📝 调用getRecentMessages API:', { limit })
const response = await http.post(`/message/user/recent`, { limit })
console.log('📝 getRecentMessages API响应:', response)
return response
// backend-single: POST /message/recent
const resp = await http.post(`/message/recent`, { limit })
console.log('📝 getRecentMessages API响应:', resp)
const data: any = (resp as any).data || resp
const records = data.records || data
return Array.isArray(records) ? records : []
},
// 获取消息详情
getMessageById: async (id: string) => {
console.log('📄 调用getMessageById API:', { id })
const response = await http.get(`/message/${id}`)
// backend-single: GET /message/detail?id=xxx
const response = await http.get(`/message/detail`, { params: { id } })
console.log('📄 getMessageById API响应:', response)
return response
}
+3 -3
View File
@@ -207,9 +207,9 @@ export class StompWebSocketService {
console.log('📤 准备发送的聊天请求:', chatRequest)
try {
// 发送到后端的/app/chat.send端点
// 发送到后端的/app/chat/send端点(对应 @MessageMapping("/chat") + @MessageMapping("/send")
this.client.publish({
destination: '/app/chat.send',
destination: '/app/chat/send',
body: JSON.stringify(chatRequest)
})
console.log('✅ STOMP聊天消息发送成功:', chatRequest)
@@ -324,7 +324,7 @@ export class StompWebSocketService {
try {
this.client.publish({
destination: '/app/chat.connect',
destination: '/app/chat/connect',
body: JSON.stringify(connectRequest)
})
console.log('✅ STOMP连接消息发送成功:', connectRequest)
+13 -9
View File
@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, nextTick } from 'vue'
import type { ChatMessage, ChatSession } from '@/types'
import { stompWebSocketService, type WebSocketMessage, type ConnectionStatus } from '@/services/stomp-websocket'
import { useAuthStore } from './auth'
@@ -381,31 +381,35 @@ export const useChatStore = defineStore('chat', () => {
}
// 添加AI回复消息(直接显示完整内容)
const addAiReplyMessages = (content: string) => {
const addAiReplyMessages = async (content: string) => {
// 停止输入状态
isTyping.value = false
// 使用 nextTick 确保 DOM 更新的顺序性,避免与定时同步并发
await nextTick()
// 直接添加完整的AI回复
const aiMessage = addMessage({
content: content.trim(),
type: 'ai',
sessionId: currentSession.value?.id
conversationId: currentSession.value?.id
})
// 强制触发响应式更新
console.log('AI消息已添加,当前消息总数:', messages.value.length)
console.log('最新AI消息:', aiMessage)
console.log('AI消息已添加,当前消息总数:', messages.value.length)
console.log('📝 最新AI消息:', aiMessage)
console.log('📊 所有消息:', messages.value)
}
// WebSocket消息处理
let handleWebSocketMessage = (wsMessage: WebSocketMessage) => {
let handleWebSocketMessage = async (wsMessage: WebSocketMessage) => {
console.log('收到WebSocket消息:', wsMessage.type, wsMessage.senderType)
switch (wsMessage.type) {
case 'TEXT':
if (wsMessage.senderType === 'AI') {
// AI回复消息 - 支持分段显示
addAiReplyMessages(wsMessage.content)
await addAiReplyMessages(wsMessage.content)
}
break
@@ -602,8 +606,8 @@ export const useChatStore = defineStore('chat', () => {
onMessage: (callback: (message: any) => void) => {
// 简单的消息监听实现
const originalHandler = handleWebSocketMessage
handleWebSocketMessage = (message: any) => {
originalHandler(message)
handleWebSocketMessage = async (message: any) => {
await originalHandler(message)
callback(message)
}
}
+35 -135
View File
@@ -67,8 +67,8 @@
<p class="mt-2 text-gray-600">加载对话记录中...</p>
</div>
<!-- 欢迎消息 -->
<div v-else-if="messages.length === 0" class="text-center py-12">
<!-- 欢迎消息使用 v-if避免与列表互斥切换引发的 DOM 竞态 -->
<div v-if="messages.length === 0" class="text-center py-12">
<img
:src="kaikaiAvatar"
alt="开开"
@@ -81,16 +81,16 @@
</p>
</div>
<!-- 消息列表 -->
<div v-else class="space-y-4">
<!-- 消息列表使用 v-show 保持节点稳定移除key避免频繁重新挂载 -->
<div v-show="messages.length > 0" class="space-y-4">
<div
v-for="(message, index) in messages"
:key="`msg-${message.id}-${index}`"
v-for="message in messages"
:key="message.id"
class="flex w-full items-end mb-4"
:class="message.role === 'user' ? 'justify-end' : 'justify-start'"
:class="message.type === 'user' ? 'justify-end' : 'justify-start'"
>
<!-- AI消息 -->
<template v-if="message.role === 'assistant'">
<template v-if="message.type === 'ai'">
<img
:src="kaikaiAvatar"
alt="开开"
@@ -105,7 +105,7 @@
</template>
<!-- 用户消息 -->
<template v-else-if="message.role === 'user'">
<template v-else-if="message.type === 'user'">
<div class="max-w-xs md:max-w-md lg:max-w-lg">
<div class="bg-tech-blue text-white rounded-l-2xl rounded-tr-2xl p-3 px-4 shadow-md">
<p class="leading-relaxed whitespace-pre-wrap">{{ message.content }}</p>
@@ -122,27 +122,6 @@
</div>
</template>
</div>
<!-- AI正在输入指示器 -->
<div v-if="chatStore.isTyping" class="flex w-full items-end justify-start">
<img
:src="kaikaiAvatar"
alt="开开"
class="w-10 h-10 rounded-full mr-3 self-start flex-shrink-0"
>
<div class="max-w-xs md:max-w-md lg:max-w-lg">
<div class="bg-white text-text-dark rounded-r-2xl rounded-tl-2xl p-3 px-4 shadow-md border border-gray-100">
<div class="flex items-center space-x-1">
<div class="flex space-x-1">
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
</div>
<span class="text-sm text-gray-500 ml-2">开开正在输入...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
@@ -193,13 +172,16 @@ import BottomNavigation from '@/components/layout/BottomNavigation.vue'
const chatStore = useChatStore()
// 响应式数据
const messages = ref<ChatMessage[]>([])
// 直接使用 chatStore.messages,避免计算属性导致的重新计算
const messages = computed(() => chatStore.messages)
const inputMessage = ref('')
const sending = ref(false)
const loading = ref(false)
const messagesContainer = ref<HTMLElement>()
const messageInput = ref<HTMLTextAreaElement>()
const lastSyncedMessageCount = ref(0) // 记录上次同步的消息数量
// 定时同步句柄(避免在 onMounted 内部注册 onUnmounted
let syncInterval: any = null
// 头像
const kaikaiAvatar = 'https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png'
@@ -267,18 +249,18 @@ const forceScrollToBottom = () => {
// 加载消息
const loadMessages = async () => {
loading.value = true
try {
// 调用最近消息API
const response = await messageApi.getRecentMessages(50)
// 提取消息数据
const messageList = response.data || response || []
if (Array.isArray(messageList)) {
// 转换消息格式
const chatMessages = MessageService.convertToChatMessages(messageList)
// 按时间排序(最早的在前面)
chatMessages.sort((a, b) => {
const parseTime = (timestamp: string | Date) => {
@@ -291,24 +273,19 @@ const loadMessages = async () => {
}
return new Date().getTime()
}
return parseTime(a.timestamp) - parseTime(b.timestamp)
})
messages.value = chatMessages
// 初始化同步计数
lastSyncedMessageCount.value = chatStore.messages.length
// 将消息添加到 chatStore
chatStore.messages.splice(0, chatStore.messages.length, ...chatMessages)
// 强制滚动到底部
await nextTick()
forceScrollToBottom()
} else {
messages.value = []
}
} catch (error) {
console.error('❌ 加载消息失败:', error)
messages.value = []
} finally {
loading.value = false
}
@@ -325,12 +302,8 @@ const sendMessage = async () => {
sending.value = true
try {
// 强制滚动到底部(为即将到来的消息做准备)
await nextTick()
forceScrollToBottom()
// 直接通过WebSocket发送消息,让chatStore处理消息添加
// 这样避免重复添加消息
// 计算属性会自动响应 chatStore 的变化
await chatStore.sendMessage(content)
} catch (error) {
@@ -340,53 +313,11 @@ const sendMessage = async () => {
}
}
// 从chatStore同步消息(完全重新构建消息列表)
const syncWithChatStore = () => {
const storeMessages = chatStore.messages
// 如果store消息数量没有变化,跳过同步
if (storeMessages.length === lastSyncedMessageCount.value) {
return
}
console.log('🔄 同步chatStore消息,数量:', storeMessages.length)
// 转换所有store消息
const convertedMessages = storeMessages.map(msg => ({
id: msg.id,
content: msg.content,
role: msg.type === 'user' ? 'user' : 'assistant',
type: msg.type,
timestamp: msg.timestamp,
status: msg.status || 'sent',
sender: msg.type === 'user' ? 'user' : 'ai'
} as ChatMessage))
// 按时间排序
convertedMessages.sort((a, b) => {
const parseTime = (timestamp: string | Date) => {
if (timestamp instanceof Date) return timestamp.getTime()
if (typeof timestamp === 'string') {
if (timestamp.includes(' ') && !timestamp.includes('T')) {
return new Date(timestamp.replace(' ', 'T')).getTime()
}
return new Date(timestamp).getTime()
}
return new Date().getTime()
}
return parseTime(a.timestamp) - parseTime(b.timestamp)
})
// 完全替换消息列表
messages.value = convertedMessages
// 更新同步计数
lastSyncedMessageCount.value = storeMessages.length
// 强制滚动到底部
nextTick(() => forceScrollToBottom())
}
// 监听消息变化,自动滚动到底部
watch(() => messages.value.length, async () => {
await nextTick()
forceScrollToBottom()
})
// 调整文本框高度
const adjustTextareaHeight = () => {
@@ -412,38 +343,16 @@ onMounted(async () => {
// 监听WebSocket消息
try {
chatStore.onMessage((message: any) => {
// 创建AI消息
const aiMessage: ChatMessage = {
id: message.id || `ai_${Date.now()}`,
content: message.content || message.message || String(message),
role: 'assistant',
type: 'ai',
timestamp: message.timestamp || new Date().toISOString(),
status: 'sent',
sender: 'ai'
}
// 添加到消息列表
messages.value.push(aiMessage)
// 强制滚动到底部
nextTick(() => forceScrollToBottom())
chatStore.onMessage(async (_message: any) => {
// 消息已经被添加到 chatStore,计算属性会自动更新
console.log('📨 Chat页面收到WebSocket消息回调')
await nextTick()
forceScrollToBottom()
})
} catch (error) {
console.warn('⚠️ 设置WebSocket监听器失败:', error)
}
// 定期同步chatStore消息(确保不遗漏)
const syncInterval = setInterval(() => {
syncWithChatStore()
}, 1000)
// 组件卸载时清理定时器
onUnmounted(() => {
clearInterval(syncInterval)
})
// 确保初始化完成后滚动到底部
await nextTick()
setTimeout(() => forceScrollToBottom(), 100)
@@ -456,18 +365,9 @@ onUnmounted(() => {
chatStore.disconnectWebSocket()
})
// 监听消息变化,自动滚动
watch(() => messages.value.length, () => {
nextTick(() => forceScrollToBottom())
})
// 监听chatStore消息变化(移除,避免与 onMessage/定时同步重复触发导致渲染竞态)
// 保留通过 onMessage 事件与定时器同步的方式,减少同一 tick 内的多次 DOM 更新
// 监听chatStore消息变化
watch(() => chatStore.messages.length, (newLength, oldLength) => {
if (newLength > oldLength) {
// 有新消息时同步
syncWithChatStore()
}
}, { immediate: false })
</script>
<style scoped>
+71
View File
@@ -0,0 +1,71 @@
<template>
<div class="forgot-page">
<div class="card">
<h2 class="title">重置密码</h2>
<el-form :model="form" :rules="rules" ref="formRef" label-width="0">
<el-form-item prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" clearable />
</el-form-item>
<el-form-item prop="newPassword">
<el-input v-model="form.newPassword" placeholder="请输入新密码" show-password clearable />
</el-form-item>
<el-form-item prop="captcha">
<el-input v-model="form.captcha" placeholder="请输入验证码(123456" clearable />
</el-form-item>
<el-button type="primary" class="w-full" :loading="submitting" @click="onSubmit">提交</el-button>
</el-form>
<div class="mt-4 text-center">
<router-link to="/login">返回登录</router-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage, FormInstance, FormRules } from 'element-plus'
import AuthService from '@/services/auth'
import type { ResetPasswordRequest } from '@/types/auth'
const formRef = ref<FormInstance>()
const submitting = ref(false)
const form = ref<ResetPasswordRequest>({ phone: '', newPassword: '', captcha: '' })
const rules: FormRules = {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: ['blur', 'change'] }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度6-20位', trigger: ['blur', 'change'] }
],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' }
]
}
const onSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
try {
submitting.value = true
await AuthService.resetPassword(form.value)
ElMessage.success('重置密码成功,请使用新密码登录')
} catch (e) {
ElMessage.error('重置密码失败,请稍后重试')
} finally {
submitting.value = false
}
})
}
</script>
<style scoped>
.forgot-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.card { width: 360px; background: #fff; padding: 24px; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
.title { text-align: center; margin-bottom: 16px; }
.w-full { width: 100%; }
</style>