优化处理

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