优化处理
This commit is contained in:
+35
-135
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user