refactor(消息组件): 重构聊天消息加载逻辑为分页加载
将消息加载逻辑从父组件移动到MessageDetail组件内部实现 添加分页加载功能,支持下拉刷新历史消息 优化消息加载状态管理和错误处理
Showing
2 changed files
with
161 additions
and
59 deletions
| ... | @@ -51,7 +51,15 @@ | ... | @@ -51,7 +51,15 @@ |
| 51 | :scroll-y="true" | 51 | :scroll-y="true" |
| 52 | :scroll-top="scrollTop" | 52 | :scroll-top="scrollTop" |
| 53 | :scroll-into-view="scrollIntoView" | 53 | :scroll-into-view="scrollIntoView" |
| 54 | + :refresher-enabled="true" | ||
| 55 | + :refresher-triggered="isLoadingMessages" | ||
| 56 | + @refresherrefresh="handleRefresh" | ||
| 54 | > | 57 | > |
| 58 | + <!-- 数据加载完成提示 --> | ||
| 59 | + <view v-if="!hasMoreMessages && !isInitialLoad && messages.length > 0 && hasTriedLoadMore" class="load-complete-tip"> | ||
| 60 | + <text class="tip-text">已加载完所有数据</text> | ||
| 61 | + </view> | ||
| 62 | + | ||
| 55 | <view | 63 | <view |
| 56 | v-for="(message, index) in messages" | 64 | v-for="(message, index) in messages" |
| 57 | :key="index" | 65 | :key="index" |
| ... | @@ -115,7 +123,7 @@ | ... | @@ -115,7 +123,7 @@ |
| 115 | <script setup> | 123 | <script setup> |
| 116 | import { ref, computed, watch, nextTick } from 'vue' | 124 | import { ref, computed, watch, nextTick } from 'vue' |
| 117 | import Taro from '@tarojs/taro' | 125 | import Taro from '@tarojs/taro' |
| 118 | -import { sendChatAPI } from '@/api/chat' | 126 | +import { sendChatAPI, getChatListAPI } from '@/api/chat' |
| 119 | import { useUserStore } from '@/stores/user' | 127 | import { useUserStore } from '@/stores/user' |
| 120 | 128 | ||
| 121 | /** | 129 | /** |
| ... | @@ -152,39 +160,101 @@ const inputMessage = ref('') | ... | @@ -152,39 +160,101 @@ const inputMessage = ref('') |
| 152 | const scrollTop = ref(0) | 160 | const scrollTop = ref(0) |
| 153 | const scrollIntoView = ref('') | 161 | const scrollIntoView = ref('') |
| 154 | 162 | ||
| 155 | -// 模拟聊天消息数据 | 163 | +// 聊天消息数据 |
| 156 | const messages = ref([]) | 164 | const messages = ref([]) |
| 157 | 165 | ||
| 166 | +// 分页相关状态 | ||
| 167 | +const PAGE_SIZE = 20 // 每页加载的消息数量 | ||
| 168 | +const currentPage = ref(0) | ||
| 169 | +const hasMoreMessages = ref(true) | ||
| 170 | +const isLoadingMessages = ref(false) | ||
| 171 | +const isInitialLoad = ref(true) | ||
| 172 | +const hasTriedLoadMore = ref(false) // 是否已经尝试过手动上拉加载更多 | ||
| 173 | + | ||
| 158 | /** | 174 | /** |
| 159 | - * 初始化聊天消息 | 175 | + * 加载聊天消息 |
| 176 | + * @param {boolean} isLoadMore - 是否为加载更多(下拉加载历史记录) | ||
| 160 | */ | 177 | */ |
| 161 | -const initChatMessages = () => { | 178 | +const loadChatMessages = async (isLoadMore = false) => { |
| 179 | + if (props.conversation?.type !== 'chat' || !props.conversation?.id) { | ||
| 180 | + return | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + // 如果正在加载,则不继续加载 | ||
| 184 | + if (isLoadingMessages.value) { | ||
| 185 | + return | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + // 对于scrolltoupper事件,如果没有更多消息则不加载 | ||
| 189 | + // 但对于下拉刷新,总是允许尝试加载 | ||
| 190 | + if (!hasMoreMessages.value && isLoadMore && !isInitialLoad.value) { | ||
| 191 | + // 这里可以根据具体需求决定是否允许重新加载 | ||
| 192 | + } | ||
| 193 | + | ||
| 162 | const userStore = useUserStore() | 194 | const userStore = useUserStore() |
| 163 | const currentUserId = userStore.userInfo?.id | 195 | const currentUserId = userStore.userInfo?.id |
| 164 | - | 196 | + |
| 165 | - if (props.conversation?.type === 'chat') { | 197 | + try { |
| 166 | - // 使用从API获取的真实聊天记录 | 198 | + isLoadingMessages.value = true |
| 167 | - if (props.conversation.chatMessages && props.conversation.chatMessages.length > 0) { | 199 | + |
| 200 | + // 如果是加载更多,页码+1 | ||
| 201 | + const page = isLoadMore ? currentPage.value + 1 : 0 | ||
| 202 | + | ||
| 203 | + const response = await getChatListAPI({ | ||
| 204 | + conversation_id: props.conversation.id, | ||
| 205 | + page: page, | ||
| 206 | + limit: PAGE_SIZE | ||
| 207 | + }) | ||
| 208 | + | ||
| 209 | + if (response.code && response.data) { | ||
| 210 | + const newMessages = response.data.list || [] | ||
| 211 | + | ||
| 168 | // 转换API数据格式为组件需要的格式 | 212 | // 转换API数据格式为组件需要的格式 |
| 169 | - messages.value = props.conversation.chatMessages.map(msg => ({ | 213 | + const formattedMessages = newMessages.map(msg => ({ |
| 170 | - type: msg.created_by === currentUserId ? 'sent' : 'received', // 根据创建者ID判断 | 214 | + type: msg.created_by === currentUserId ? 'sent' : 'received', |
| 171 | content: msg.note || '', | 215 | content: msg.note || '', |
| 172 | time: msg.created_time_desc || '', | 216 | time: msg.created_time_desc || '', |
| 173 | - id: msg.id | 217 | + id: msg.id, |
| 218 | + created_by: msg.created_by, | ||
| 219 | + create_time: msg.create_time | ||
| 174 | })) | 220 | })) |
| 175 | - } else { | 221 | + |
| 176 | - // 如果没有历史消息,显示欢迎消息 | 222 | + if (isLoadMore) { |
| 177 | - messages.value = [ | 223 | + // 加载更多时,将新消息添加到列表顶部(历史消息) |
| 178 | - { | 224 | + messages.value = [...formattedMessages, ...messages.value] |
| 179 | - type: 'received', | 225 | + currentPage.value = page |
| 180 | - content: '您好,有什么可以帮助您的吗?', | 226 | + } else { |
| 181 | - time: new Date().toLocaleTimeString('zh-CN', { | 227 | + // 初始加载时,直接设置消息列表 |
| 182 | - hour: '2-digit', | 228 | + messages.value = formattedMessages |
| 183 | - minute: '2-digit' | 229 | + currentPage.value = 0 |
| 184 | - }) | 230 | + isInitialLoad.value = false |
| 185 | - } | 231 | + } |
| 186 | - ] | 232 | + |
| 233 | + // 判断是否还有更多消息 | ||
| 234 | + hasMoreMessages.value = newMessages.length >= PAGE_SIZE | ||
| 235 | + | ||
| 236 | + // 如果没有任何消息,显示欢迎消息 | ||
| 237 | + if (messages.value.length === 0 && !isLoadMore) { | ||
| 238 | + messages.value = [ | ||
| 239 | + { | ||
| 240 | + type: 'received', | ||
| 241 | + content: '您好,有什么可以帮助您的吗?', | ||
| 242 | + time: new Date().toLocaleTimeString('zh-CN', { | ||
| 243 | + hour: '2-digit', | ||
| 244 | + minute: '2-digit' | ||
| 245 | + }) | ||
| 246 | + } | ||
| 247 | + ] | ||
| 248 | + } | ||
| 187 | } | 249 | } |
| 250 | + } catch (error) { | ||
| 251 | + console.error('加载聊天消息失败:', error) | ||
| 252 | + Taro.showToast({ | ||
| 253 | + title: '加载消息失败', | ||
| 254 | + icon: 'error' | ||
| 255 | + }) | ||
| 256 | + } finally { | ||
| 257 | + isLoadingMessages.value = false | ||
| 188 | } | 258 | } |
| 189 | } | 259 | } |
| 190 | 260 | ||
| ... | @@ -210,9 +280,9 @@ const sendMessage = async () => { | ... | @@ -210,9 +280,9 @@ const sendMessage = async () => { |
| 210 | } | 280 | } |
| 211 | 281 | ||
| 212 | const messageContent = inputMessage.value.trim() | 282 | const messageContent = inputMessage.value.trim() |
| 213 | - | 283 | + |
| 214 | try { | 284 | try { |
| 215 | - // 先添加到本地显示 | 285 | + // 先添加到本地显示(新消息添加到末尾) |
| 216 | const newMessage = { | 286 | const newMessage = { |
| 217 | type: 'sent', | 287 | type: 'sent', |
| 218 | content: messageContent, | 288 | content: messageContent, |
| ... | @@ -222,32 +292,32 @@ const sendMessage = async () => { | ... | @@ -222,32 +292,32 @@ const sendMessage = async () => { |
| 222 | }) | 292 | }) |
| 223 | } | 293 | } |
| 224 | messages.value.push(newMessage) | 294 | messages.value.push(newMessage) |
| 225 | - | 295 | + |
| 226 | // 清空输入框 | 296 | // 清空输入框 |
| 227 | inputMessage.value = '' | 297 | inputMessage.value = '' |
| 228 | - | 298 | + |
| 229 | // 滚动到底部 | 299 | // 滚动到底部 |
| 230 | await nextTick() | 300 | await nextTick() |
| 231 | scrollToBottom() | 301 | scrollToBottom() |
| 232 | - | 302 | + |
| 233 | // 调用API发送消息 | 303 | // 调用API发送消息 |
| 234 | const response = await sendChatAPI({ | 304 | const response = await sendChatAPI({ |
| 235 | conversation_id: props.conversation.id, | 305 | conversation_id: props.conversation.id, |
| 236 | note: messageContent | 306 | note: messageContent |
| 237 | }) | 307 | }) |
| 238 | - | 308 | + |
| 239 | if (response.code) { | 309 | if (response.code) { |
| 240 | // 发送成功,更新消息ID | 310 | // 发送成功,更新消息ID |
| 241 | if (response.data && response.data.id) { | 311 | if (response.data && response.data.id) { |
| 242 | newMessage.id = response.data.id | 312 | newMessage.id = response.data.id |
| 243 | } | 313 | } |
| 244 | - | 314 | + |
| 245 | // 触发发送消息事件,通知父组件更新列表 | 315 | // 触发发送消息事件,通知父组件更新列表 |
| 246 | emit('sendMessage', { | 316 | emit('sendMessage', { |
| 247 | conversation: props.conversation, | 317 | conversation: props.conversation, |
| 248 | message: messageContent | 318 | message: messageContent |
| 249 | }) | 319 | }) |
| 250 | - | 320 | + |
| 251 | Taro.showToast({ | 321 | Taro.showToast({ |
| 252 | title: '发送成功', | 322 | title: '发送成功', |
| 253 | icon: 'success' | 323 | icon: 'success' |
| ... | @@ -272,6 +342,20 @@ const sendMessage = async () => { | ... | @@ -272,6 +342,20 @@ const sendMessage = async () => { |
| 272 | } | 342 | } |
| 273 | 343 | ||
| 274 | /** | 344 | /** |
| 345 | + * 处理下拉刷新 | ||
| 346 | + */ | ||
| 347 | +const handleRefresh = async () => { | ||
| 348 | + if (!isLoadingMessages.value) { | ||
| 349 | + hasTriedLoadMore.value = true // 标记用户已经尝试手动加载更多 | ||
| 350 | + await loadChatMessages(true) | ||
| 351 | + } | ||
| 352 | + // 确保刷新状态被重置 | ||
| 353 | + setTimeout(() => { | ||
| 354 | + isLoadingMessages.value = false | ||
| 355 | + }, 100) | ||
| 356 | +} | ||
| 357 | + | ||
| 358 | +/** | ||
| 275 | * 滚动到底部 | 359 | * 滚动到底部 |
| 276 | */ | 360 | */ |
| 277 | const scrollToBottom = () => { | 361 | const scrollToBottom = () => { |
| ... | @@ -305,17 +389,29 @@ const handleClose = () => { | ... | @@ -305,17 +389,29 @@ const handleClose = () => { |
| 305 | scrollTop.value = 0 | 389 | scrollTop.value = 0 |
| 306 | scrollIntoView.value = '' | 390 | scrollIntoView.value = '' |
| 307 | messages.value = [] | 391 | messages.value = [] |
| 392 | + // 重置分页状态 | ||
| 393 | + currentPage.value = 0 | ||
| 394 | + hasMoreMessages.value = true | ||
| 395 | + isLoadingMessages.value = false | ||
| 396 | + isInitialLoad.value = true | ||
| 397 | + hasTriedLoadMore.value = false // 重置手动加载标记 | ||
| 308 | emit('close') | 398 | emit('close') |
| 309 | } | 399 | } |
| 310 | 400 | ||
| 311 | /** | 401 | /** |
| 312 | - * 监听对话变化,初始化消息 | 402 | + * 监听对话变化,加载消息 |
| 313 | */ | 403 | */ |
| 314 | watch( | 404 | watch( |
| 315 | () => props.conversation, | 405 | () => props.conversation, |
| 316 | - (newConversation) => { | 406 | + async (newConversation) => { |
| 317 | if (newConversation && visible.value) { | 407 | if (newConversation && visible.value) { |
| 318 | - initChatMessages() | 408 | + // 重置状态 |
| 409 | + currentPage.value = 0 | ||
| 410 | + hasMoreMessages.value = true | ||
| 411 | + isInitialLoad.value = true | ||
| 412 | + hasTriedLoadMore.value = false // 重置手动加载标记 | ||
| 413 | + | ||
| 414 | + await loadChatMessages() | ||
| 319 | // 延迟滚动确保消息渲染完成 | 415 | // 延迟滚动确保消息渲染完成 |
| 320 | setTimeout(() => { | 416 | setTimeout(() => { |
| 321 | nextTick(() => { | 417 | nextTick(() => { |
| ... | @@ -330,9 +426,15 @@ watch( | ... | @@ -330,9 +426,15 @@ watch( |
| 330 | /** | 426 | /** |
| 331 | * 监听弹框显示状态 | 427 | * 监听弹框显示状态 |
| 332 | */ | 428 | */ |
| 333 | -watch(visible, (newVisible) => { | 429 | +watch(visible, async (newVisible) => { |
| 334 | if (newVisible && props.conversation) { | 430 | if (newVisible && props.conversation) { |
| 335 | - initChatMessages() | 431 | + // 重置状态 |
| 432 | + currentPage.value = 0 | ||
| 433 | + hasMoreMessages.value = true | ||
| 434 | + isInitialLoad.value = true | ||
| 435 | + hasTriedLoadMore.value = false // 重置手动加载标记 | ||
| 436 | + | ||
| 437 | + await loadChatMessages() | ||
| 336 | // 确保弹框完全打开后再滚动到底部 | 438 | // 确保弹框完全打开后再滚动到底部 |
| 337 | setTimeout(() => { | 439 | setTimeout(() => { |
| 338 | nextTick(() => { | 440 | nextTick(() => { |
| ... | @@ -376,18 +478,18 @@ watch(visible, (newVisible) => { | ... | @@ -376,18 +478,18 @@ watch(visible, (newVisible) => { |
| 376 | border-radius: 16rpx; | 478 | border-radius: 16rpx; |
| 377 | margin-bottom: 24rpx; | 479 | margin-bottom: 24rpx; |
| 378 | box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); | 480 | box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); |
| 379 | - | 481 | + |
| 380 | .message-title { | 482 | .message-title { |
| 381 | margin-bottom: 16rpx; | 483 | margin-bottom: 16rpx; |
| 382 | padding-bottom: 12rpx; | 484 | padding-bottom: 12rpx; |
| 383 | border-bottom: 1rpx solid #f0f0f0; | 485 | border-bottom: 1rpx solid #f0f0f0; |
| 384 | } | 486 | } |
| 385 | - | 487 | + |
| 386 | .message-body { | 488 | .message-body { |
| 387 | line-height: 1.6; | 489 | line-height: 1.6; |
| 388 | margin-bottom: 16rpx; | 490 | margin-bottom: 16rpx; |
| 389 | } | 491 | } |
| 390 | - | 492 | + |
| 391 | .message-meta { | 493 | .message-meta { |
| 392 | padding-top: 12rpx; | 494 | padding-top: 12rpx; |
| 393 | border-top: 1rpx solid #f0f0f0; | 495 | border-top: 1rpx solid #f0f0f0; |
| ... | @@ -408,6 +510,22 @@ watch(visible, (newVisible) => { | ... | @@ -408,6 +510,22 @@ watch(visible, (newVisible) => { |
| 408 | overflow-y: auto; | 510 | overflow-y: auto; |
| 409 | } | 511 | } |
| 410 | 512 | ||
| 513 | +// 数据加载完成提示样式 | ||
| 514 | +.load-complete-tip { | ||
| 515 | + text-align: center; | ||
| 516 | + padding: 20rpx 0; | ||
| 517 | + margin-bottom: 16rpx; | ||
| 518 | + | ||
| 519 | + .tip-text { | ||
| 520 | + font-size: 24rpx; | ||
| 521 | + color: #999999; | ||
| 522 | + background: #f5f5f5; | ||
| 523 | + padding: 12rpx 24rpx; | ||
| 524 | + border-radius: 20rpx; | ||
| 525 | + display: inline-block; | ||
| 526 | + } | ||
| 527 | +} | ||
| 528 | + | ||
| 411 | .message-item { | 529 | .message-item { |
| 412 | margin-bottom: 24rpx; | 530 | margin-bottom: 24rpx; |
| 413 | display: flex; | 531 | display: flex; | ... | ... |
| ... | @@ -103,7 +103,7 @@ import MessageDetail from '@/components/MessageDetail.vue' | ... | @@ -103,7 +103,7 @@ import MessageDetail from '@/components/MessageDetail.vue' |
| 103 | import { $ } from '@tarojs/extend' | 103 | import { $ } from '@tarojs/extend' |
| 104 | import Taro from '@tarojs/taro' | 104 | import Taro from '@tarojs/taro' |
| 105 | // 导入接口 | 105 | // 导入接口 |
| 106 | -import { getMessagesListAPI, getMessagesDetailAPI, getChatListAPI } from '@/api/chat' | 106 | +import { getMessagesListAPI, getMessagesDetailAPI } from '@/api/chat' |
| 107 | 107 | ||
| 108 | // 默认头像 | 108 | // 默认头像 |
| 109 | const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | 109 | const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' |
| ... | @@ -335,7 +335,7 @@ const loadMore = async () => { | ... | @@ -335,7 +335,7 @@ const loadMore = async () => { |
| 335 | const onConversationClick = async (conversation) => { | 335 | const onConversationClick = async (conversation) => { |
| 336 | try { | 336 | try { |
| 337 | loading.value = true | 337 | loading.value = true |
| 338 | - | 338 | + |
| 339 | // 根据消息类型获取详情数据 | 339 | // 根据消息类型获取详情数据 |
| 340 | if (conversation.type === 'system') { | 340 | if (conversation.type === 'system') { |
| 341 | // 获取系统消息详情 | 341 | // 获取系统消息详情 |
| ... | @@ -353,24 +353,8 @@ const onConversationClick = async (conversation) => { | ... | @@ -353,24 +353,8 @@ const onConversationClick = async (conversation) => { |
| 353 | selectedConversation.value = conversation | 353 | selectedConversation.value = conversation |
| 354 | } | 354 | } |
| 355 | } else if (conversation.type === 'chat') { | 355 | } else if (conversation.type === 'chat') { |
| 356 | - // 获取聊天消息列表 | 356 | + // 聊天类型直接传递conversation,消息加载在MessageDetail组件内部处理 |
| 357 | - const response = await getChatListAPI({ | 357 | + selectedConversation.value = conversation |
| 358 | - conversation_id: conversation.id, | ||
| 359 | - page: 0, | ||
| 360 | - limit: 50 | ||
| 361 | - }) | ||
| 362 | - if (response.code && response.data) { | ||
| 363 | - // 更新conversation数据,包含聊天记录和接收者信息 | ||
| 364 | - selectedConversation.value = { | ||
| 365 | - ...conversation, | ||
| 366 | - chatMessages: response.data.list || [], | ||
| 367 | - receiver: response.data.receiver || {}, | ||
| 368 | - name: response.data.receiver?.nickname || conversation.name, | ||
| 369 | - avatar: response.data.receiver?.avatar || conversation.avatar | ||
| 370 | - } | ||
| 371 | - } else { | ||
| 372 | - selectedConversation.value = conversation | ||
| 373 | - } | ||
| 374 | } else { | 358 | } else { |
| 375 | selectedConversation.value = conversation | 359 | selectedConversation.value = conversation |
| 376 | } | 360 | } | ... | ... |
-
Please register or login to post a comment