hookehuyr

refactor(消息组件): 重构聊天消息加载逻辑为分页加载

将消息加载逻辑从父组件移动到MessageDetail组件内部实现
添加分页加载功能,支持下拉刷新历史消息
优化消息加载状态管理和错误处理
...@@ -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 }
......