feat(消息): 新增消息详情组件并重构消息页面
将消息详情弹框抽离为独立组件 MessageDetail,支持消息发送功能 重构消息页面代码,移除冗余样式,优化消息列表布局
Showing
3 changed files
with
596 additions
and
125 deletions
| ... | @@ -10,6 +10,7 @@ declare module 'vue' { | ... | @@ -10,6 +10,7 @@ declare module 'vue' { |
| 10 | BrandModelPicker: typeof import('./src/components/BrandModelPicker.vue')['default'] | 10 | BrandModelPicker: typeof import('./src/components/BrandModelPicker.vue')['default'] |
| 11 | FeaturedRecommendations: typeof import('./src/components/FeaturedRecommendations.vue')['default'] | 11 | FeaturedRecommendations: typeof import('./src/components/FeaturedRecommendations.vue')['default'] |
| 12 | LatestScooters: typeof import('./src/components/LatestScooters.vue')['default'] | 12 | LatestScooters: typeof import('./src/components/LatestScooters.vue')['default'] |
| 13 | + MessageDetail: typeof import('./src/components/MessageDetail.vue')['default'] | ||
| 13 | NavBar: typeof import('./src/components/navBar.vue')['default'] | 14 | NavBar: typeof import('./src/components/navBar.vue')['default'] |
| 14 | NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet'] | 15 | NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet'] |
| 15 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | 16 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | ... | ... |
src/components/MessageDetail.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <nut-popup | ||
| 3 | + v-model:visible="visible" | ||
| 4 | + position="right" | ||
| 5 | + :style="{ width: '100%', height: '100%' }" | ||
| 6 | + closeable | ||
| 7 | + close-icon-position="top-right" | ||
| 8 | + @close="handleClose" | ||
| 9 | + > | ||
| 10 | + <view class="message-detail-container"> | ||
| 11 | + <!-- 详情页头部 --> | ||
| 12 | + <view class="detail-header"> | ||
| 13 | + <view class="flex items-center"> | ||
| 14 | + <image | ||
| 15 | + v-if="conversation?.avatar" | ||
| 16 | + :src="conversation.avatar" | ||
| 17 | + class="w-12 h-12 rounded-full object-cover mr-3" | ||
| 18 | + mode="aspectFill" | ||
| 19 | + /> | ||
| 20 | + <view v-else class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center mr-3"> | ||
| 21 | + <component :is="conversation?.icon" /> | ||
| 22 | + </view> | ||
| 23 | + <view class="flex-1"> | ||
| 24 | + <text class="text-lg font-medium">{{ conversation?.name }}</text> | ||
| 25 | + <text class="text-sm text-gray-500 block">{{ conversation?.time }}</text> | ||
| 26 | + </view> | ||
| 27 | + </view> | ||
| 28 | + </view> | ||
| 29 | + | ||
| 30 | + <!-- 消息内容区域 --> | ||
| 31 | + <view class="detail-content"> | ||
| 32 | + <!-- 系统通知样式 --> | ||
| 33 | + <view v-if="conversation?.type === 'notification'" class="notification-content"> | ||
| 34 | + <view class="message-content"> | ||
| 35 | + <text class="text-base">{{ conversation?.lastMessage }}</text> | ||
| 36 | + </view> | ||
| 37 | + </view> | ||
| 38 | + | ||
| 39 | + <!-- 聊天记录样式 --> | ||
| 40 | + <view v-else class="chat-content"> | ||
| 41 | + <scroll-view | ||
| 42 | + class="chat-messages" | ||
| 43 | + :scroll-y="true" | ||
| 44 | + :scroll-top="scrollTop" | ||
| 45 | + :scroll-into-view="scrollIntoView" | ||
| 46 | + > | ||
| 47 | + <view | ||
| 48 | + v-for="(message, index) in messages" | ||
| 49 | + :key="index" | ||
| 50 | + :id="`msg-${index}`" | ||
| 51 | + class="message-item" | ||
| 52 | + :class="{ | ||
| 53 | + 'message-sent': message.type === 'sent', | ||
| 54 | + 'message-received': message.type === 'received' | ||
| 55 | + }" | ||
| 56 | + > | ||
| 57 | + <view class="message-bubble"> | ||
| 58 | + <text class="message-text">{{ message.content }}</text> | ||
| 59 | + <text class="message-time">{{ message.time }}</text> | ||
| 60 | + </view> | ||
| 61 | + </view> | ||
| 62 | + </scroll-view> | ||
| 63 | + | ||
| 64 | + <!-- 输入框区域 --> | ||
| 65 | + <view class="chat-input-area"> | ||
| 66 | + <view class="input-container"> | ||
| 67 | + <nut-textarea | ||
| 68 | + v-model="inputMessage" | ||
| 69 | + placeholder="请输入回复内容..." | ||
| 70 | + :rows="2" | ||
| 71 | + :max-length="500" | ||
| 72 | + class="message-input" | ||
| 73 | + @focus="handleInputFocus" | ||
| 74 | + /> | ||
| 75 | + <nut-button | ||
| 76 | + type="primary" | ||
| 77 | + size="small" | ||
| 78 | + color="orange" | ||
| 79 | + @click="sendMessage" | ||
| 80 | + :disabled="!inputMessage.trim()" | ||
| 81 | + class="send-button" | ||
| 82 | + > | ||
| 83 | + 发送 | ||
| 84 | + </nut-button> | ||
| 85 | + </view> | ||
| 86 | + </view> | ||
| 87 | + </view> | ||
| 88 | + </view> | ||
| 89 | + | ||
| 90 | + <!-- 底部按钮 --> | ||
| 91 | + <view class="detail-footer"> | ||
| 92 | + <nut-button | ||
| 93 | + type="primary" | ||
| 94 | + size="large" | ||
| 95 | + block | ||
| 96 | + @click="handleClose" | ||
| 97 | + color="orange" | ||
| 98 | + > | ||
| 99 | + {{ conversation?.type === 'notification' ? '关闭' : '返回' }} | ||
| 100 | + </nut-button> | ||
| 101 | + </view> | ||
| 102 | + </view> | ||
| 103 | + </nut-popup> | ||
| 104 | +</template> | ||
| 105 | + | ||
| 106 | +<script setup> | ||
| 107 | +import { ref, computed, watch, nextTick } from 'vue' | ||
| 108 | +import Taro from '@tarojs/taro' | ||
| 109 | + | ||
| 110 | +/** | ||
| 111 | + * 消息详情组件 Props | ||
| 112 | + */ | ||
| 113 | +const props = defineProps({ | ||
| 114 | + // 弹框显示状态 | ||
| 115 | + modelValue: { | ||
| 116 | + type: Boolean, | ||
| 117 | + default: false | ||
| 118 | + }, | ||
| 119 | + // 当前对话信息 | ||
| 120 | + conversation: { | ||
| 121 | + type: Object, | ||
| 122 | + default: () => ({}) | ||
| 123 | + } | ||
| 124 | +}) | ||
| 125 | + | ||
| 126 | +/** | ||
| 127 | + * 组件事件 | ||
| 128 | + */ | ||
| 129 | +const emit = defineEmits(['update:modelValue', 'close', 'sendMessage']) | ||
| 130 | + | ||
| 131 | +// 弹框显示状态 | ||
| 132 | +const visible = computed({ | ||
| 133 | + get: () => props.modelValue, | ||
| 134 | + set: (value) => emit('update:modelValue', value) | ||
| 135 | +}) | ||
| 136 | + | ||
| 137 | +// 输入消息内容 | ||
| 138 | +const inputMessage = ref('') | ||
| 139 | + | ||
| 140 | +// 滚动相关 | ||
| 141 | +const scrollTop = ref(0) | ||
| 142 | +const scrollIntoView = ref('') | ||
| 143 | + | ||
| 144 | +// 模拟聊天消息数据 | ||
| 145 | +const messages = ref([]) | ||
| 146 | + | ||
| 147 | +/** | ||
| 148 | + * 初始化聊天消息 | ||
| 149 | + */ | ||
| 150 | +const initChatMessages = () => { | ||
| 151 | + if (props.conversation?.type === 'message' || props.conversation?.type === 'chat') { | ||
| 152 | + // 模拟历史消息 | ||
| 153 | + messages.value = [ | ||
| 154 | + { | ||
| 155 | + type: 'received', | ||
| 156 | + content: props.conversation.lastMessage || '您好,有什么可以帮助您的吗?', | ||
| 157 | + time: '10:30' | ||
| 158 | + }, | ||
| 159 | + { | ||
| 160 | + type: 'sent', | ||
| 161 | + content: '我想咨询一下车辆的相关问题', | ||
| 162 | + time: '10:32' | ||
| 163 | + }, | ||
| 164 | + { | ||
| 165 | + type: 'received', | ||
| 166 | + content: '好的,请问您具体想了解哪方面的信息呢?我会尽力为您解答。', | ||
| 167 | + time: '10:33' | ||
| 168 | + }, | ||
| 169 | + { | ||
| 170 | + type: 'sent', | ||
| 171 | + content: '我想咨询一下车辆的相关问题', | ||
| 172 | + time: '10:34' | ||
| 173 | + }, | ||
| 174 | + { | ||
| 175 | + type: 'received', | ||
| 176 | + content: '好的,请问您具体想了解哪方面的信息呢?我会尽力为您解答。', | ||
| 177 | + time: '10:35' | ||
| 178 | + }, | ||
| 179 | + { | ||
| 180 | + type: 'sent', | ||
| 181 | + content: '我想咨询一下车辆的相关问题', | ||
| 182 | + time: '10:36' | ||
| 183 | + }, | ||
| 184 | + { | ||
| 185 | + type: 'received', | ||
| 186 | + content: '好的,请问您具体想了解哪方面的信息呢?我会尽力为您解答。', | ||
| 187 | + time: '10:37' | ||
| 188 | + }, | ||
| 189 | + ] | ||
| 190 | + } | ||
| 191 | +} | ||
| 192 | + | ||
| 193 | +/** | ||
| 194 | + * 发送消息 | ||
| 195 | + */ | ||
| 196 | +const sendMessage = async () => { | ||
| 197 | + if (!inputMessage.value.trim()) { | ||
| 198 | + Taro.showToast({ | ||
| 199 | + title: '请输入消息内容', | ||
| 200 | + icon: 'none' | ||
| 201 | + }) | ||
| 202 | + return | ||
| 203 | + } | ||
| 204 | + | ||
| 205 | + const newMessage = { | ||
| 206 | + type: 'sent', | ||
| 207 | + content: inputMessage.value.trim(), | ||
| 208 | + time: new Date().toLocaleTimeString('zh-CN', { | ||
| 209 | + hour: '2-digit', | ||
| 210 | + minute: '2-digit' | ||
| 211 | + }) | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + messages.value.push(newMessage) | ||
| 215 | + | ||
| 216 | + // 清空输入框 | ||
| 217 | + const messageContent = inputMessage.value | ||
| 218 | + inputMessage.value = '' | ||
| 219 | + | ||
| 220 | + // 滚动到底部 | ||
| 221 | + await nextTick() | ||
| 222 | + scrollToBottom() | ||
| 223 | + | ||
| 224 | + // 触发发送消息事件 | ||
| 225 | + emit('sendMessage', { | ||
| 226 | + conversation: props.conversation, | ||
| 227 | + message: messageContent | ||
| 228 | + }) | ||
| 229 | + | ||
| 230 | + // 模拟对方回复 | ||
| 231 | + setTimeout(() => { | ||
| 232 | + const replyMessage = { | ||
| 233 | + type: 'received', | ||
| 234 | + content: '收到您的消息,我们会尽快处理并回复您。', | ||
| 235 | + time: new Date().toLocaleTimeString('zh-CN', { | ||
| 236 | + hour: '2-digit', | ||
| 237 | + minute: '2-digit' | ||
| 238 | + }) | ||
| 239 | + } | ||
| 240 | + messages.value.push(replyMessage) | ||
| 241 | + nextTick(() => { | ||
| 242 | + scrollToBottom() | ||
| 243 | + }) | ||
| 244 | + }, 1000) | ||
| 245 | +} | ||
| 246 | + | ||
| 247 | +/** | ||
| 248 | + * 滚动到底部 | ||
| 249 | + */ | ||
| 250 | +const scrollToBottom = () => { | ||
| 251 | + if (messages.value.length > 0) { | ||
| 252 | + // 使用 setTimeout 确保 DOM 更新完成后再滚动 | ||
| 253 | + setTimeout(() => { | ||
| 254 | + scrollIntoView.value = `msg-${messages.value.length - 1}` | ||
| 255 | + }, 100) | ||
| 256 | + } | ||
| 257 | +} | ||
| 258 | + | ||
| 259 | +/** | ||
| 260 | + * 输入框获得焦点 | ||
| 261 | + */ | ||
| 262 | +const handleInputFocus = () => { | ||
| 263 | + setTimeout(() => { | ||
| 264 | + scrollToBottom() | ||
| 265 | + }, 300) | ||
| 266 | +} | ||
| 267 | + | ||
| 268 | +/** | ||
| 269 | + * 关闭弹框 | ||
| 270 | + */ | ||
| 271 | +const handleClose = () => { | ||
| 272 | + visible.value = false | ||
| 273 | + inputMessage.value = '' | ||
| 274 | + emit('close') | ||
| 275 | +} | ||
| 276 | + | ||
| 277 | +/** | ||
| 278 | + * 监听对话变化,初始化消息 | ||
| 279 | + */ | ||
| 280 | +watch( | ||
| 281 | + () => props.conversation, | ||
| 282 | + (newConversation) => { | ||
| 283 | + if (newConversation && visible.value) { | ||
| 284 | + initChatMessages() | ||
| 285 | + // 延迟滚动确保消息渲染完成 | ||
| 286 | + setTimeout(() => { | ||
| 287 | + nextTick(() => { | ||
| 288 | + scrollToBottom() | ||
| 289 | + }) | ||
| 290 | + }, 200) | ||
| 291 | + } | ||
| 292 | + }, | ||
| 293 | + { immediate: true } | ||
| 294 | +) | ||
| 295 | + | ||
| 296 | +/** | ||
| 297 | + * 监听弹框显示状态 | ||
| 298 | + */ | ||
| 299 | +watch(visible, (newVisible) => { | ||
| 300 | + if (newVisible && props.conversation) { | ||
| 301 | + initChatMessages() | ||
| 302 | + // 确保弹框完全打开后再滚动到底部 | ||
| 303 | + setTimeout(() => { | ||
| 304 | + nextTick(() => { | ||
| 305 | + scrollToBottom() | ||
| 306 | + }) | ||
| 307 | + }, 300) | ||
| 308 | + } | ||
| 309 | +}) | ||
| 310 | +</script> | ||
| 311 | + | ||
| 312 | +<style lang="less"> | ||
| 313 | +.message-detail-container { | ||
| 314 | + height: 100%; | ||
| 315 | + display: flex; | ||
| 316 | + flex-direction: column; | ||
| 317 | + background: #ffffff; | ||
| 318 | +} | ||
| 319 | + | ||
| 320 | +.detail-header { | ||
| 321 | + padding: 32rpx 24rpx; | ||
| 322 | + border-bottom: 1rpx solid #f0f0f0; | ||
| 323 | + background: #ffffff; | ||
| 324 | + flex-shrink: 0; | ||
| 325 | +} | ||
| 326 | + | ||
| 327 | +.detail-content { | ||
| 328 | + flex: 1; | ||
| 329 | + overflow: hidden; | ||
| 330 | + background: #f8f9fa; | ||
| 331 | +} | ||
| 332 | + | ||
| 333 | +// 系统通知样式 | ||
| 334 | +.notification-content { | ||
| 335 | + padding: 24rpx; | ||
| 336 | + height: 100%; | ||
| 337 | + overflow-y: auto; | ||
| 338 | + | ||
| 339 | + .message-content { | ||
| 340 | + background: #ffffff; | ||
| 341 | + padding: 24rpx; | ||
| 342 | + border-radius: 16rpx; | ||
| 343 | + margin-bottom: 24rpx; | ||
| 344 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); | ||
| 345 | + } | ||
| 346 | +} | ||
| 347 | + | ||
| 348 | +// 聊天记录样式 | ||
| 349 | +.chat-content { | ||
| 350 | + height: 100%; | ||
| 351 | + display: flex; | ||
| 352 | + flex-direction: column; | ||
| 353 | +} | ||
| 354 | + | ||
| 355 | +.chat-messages { | ||
| 356 | + flex: 1; | ||
| 357 | + padding: 24rpx; | ||
| 358 | + overflow-y: auto; | ||
| 359 | +} | ||
| 360 | + | ||
| 361 | +.message-item { | ||
| 362 | + margin-bottom: 24rpx; | ||
| 363 | + display: flex; | ||
| 364 | + padding: 0 8rpx; // 添加左右内边距防止遮挡 | ||
| 365 | + | ||
| 366 | + &.message-sent { | ||
| 367 | + justify-content: flex-end; | ||
| 368 | + padding-right: 60rpx; // 右侧消息增加右边距 | ||
| 369 | + | ||
| 370 | + .message-bubble { | ||
| 371 | + background: #f97316; | ||
| 372 | + color: #ffffff; | ||
| 373 | + border-radius: 20rpx 20rpx 8rpx 20rpx; | ||
| 374 | + max-width: 65%; // 减少最大宽度,留出更多空间 | ||
| 375 | + } | ||
| 376 | + } | ||
| 377 | + | ||
| 378 | + &.message-received { | ||
| 379 | + justify-content: flex-start; | ||
| 380 | + padding-left: 16rpx; // 左侧消息增加左边距 | ||
| 381 | + | ||
| 382 | + .message-bubble { | ||
| 383 | + background: #ffffff; | ||
| 384 | + color: #333333; | ||
| 385 | + border-radius: 20rpx 20rpx 20rpx 8rpx; | ||
| 386 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); | ||
| 387 | + max-width: 65%; // 减少最大宽度,保持一致 | ||
| 388 | + } | ||
| 389 | + } | ||
| 390 | +} | ||
| 391 | + | ||
| 392 | +.message-bubble { | ||
| 393 | + padding: 16rpx 20rpx; | ||
| 394 | + position: relative; | ||
| 395 | + word-wrap: break-word; | ||
| 396 | + overflow-wrap: break-word; | ||
| 397 | +} | ||
| 398 | + | ||
| 399 | +.message-text { | ||
| 400 | + font-size: 30rpx; | ||
| 401 | + line-height: 1.5; | ||
| 402 | + word-break: break-word; | ||
| 403 | + display: block; | ||
| 404 | +} | ||
| 405 | + | ||
| 406 | +.message-time { | ||
| 407 | + font-size: 22rpx; | ||
| 408 | + opacity: 0.7; | ||
| 409 | + margin-top: 8rpx; | ||
| 410 | + display: block; | ||
| 411 | +} | ||
| 412 | + | ||
| 413 | +.chat-input-area { | ||
| 414 | + background: #ffffff; | ||
| 415 | + border-top: 1rpx solid #f0f0f0; | ||
| 416 | + flex-shrink: 0; | ||
| 417 | +} | ||
| 418 | + | ||
| 419 | +.input-container { | ||
| 420 | + padding: 24rpx; | ||
| 421 | + display: flex; | ||
| 422 | + align-items: flex-end; | ||
| 423 | + gap: 16rpx; | ||
| 424 | +} | ||
| 425 | + | ||
| 426 | +.message-input { | ||
| 427 | + flex: 1; | ||
| 428 | + min-height: 80rpx; | ||
| 429 | + max-height: 160rpx; | ||
| 430 | +} | ||
| 431 | + | ||
| 432 | +.send-button { | ||
| 433 | + height: 80rpx; | ||
| 434 | + padding: 0 24rpx; | ||
| 435 | + flex-shrink: 0; | ||
| 436 | +} | ||
| 437 | + | ||
| 438 | +.detail-footer { | ||
| 439 | + padding: 24rpx; | ||
| 440 | + background: #ffffff; | ||
| 441 | + border-top: 1rpx solid #f0f0f0; | ||
| 442 | + flex-shrink: 0; | ||
| 443 | +} | ||
| 444 | + | ||
| 445 | +// Tailwind CSS 类的补充样式 | ||
| 446 | +// .w-12 { | ||
| 447 | +// width: 88rpx; | ||
| 448 | +// } | ||
| 449 | + | ||
| 450 | +// .h-12 { | ||
| 451 | +// height: 88rpx; | ||
| 452 | +// } | ||
| 453 | + | ||
| 454 | +// .rounded-full { | ||
| 455 | +// border-radius: 50%; | ||
| 456 | +// } | ||
| 457 | + | ||
| 458 | +// .object-cover { | ||
| 459 | +// object-fit: cover; | ||
| 460 | +// } | ||
| 461 | + | ||
| 462 | +// .bg-gray-100 { | ||
| 463 | +// background-color: #f3f4f6; | ||
| 464 | +// } | ||
| 465 | + | ||
| 466 | +// .flex { | ||
| 467 | +// display: flex; | ||
| 468 | +// } | ||
| 469 | + | ||
| 470 | +// .flex-1 { | ||
| 471 | +// flex: 1; | ||
| 472 | +// } | ||
| 473 | + | ||
| 474 | +// .items-center { | ||
| 475 | +// align-items: center; | ||
| 476 | +// } | ||
| 477 | + | ||
| 478 | +// .mr-3 { | ||
| 479 | +// margin-right: 24rpx; | ||
| 480 | +// } | ||
| 481 | + | ||
| 482 | +// .font-medium { | ||
| 483 | +// font-weight: 500; | ||
| 484 | +// } | ||
| 485 | + | ||
| 486 | +// .text-lg { | ||
| 487 | +// font-size: 36rpx; | ||
| 488 | +// } | ||
| 489 | + | ||
| 490 | +// .text-sm { | ||
| 491 | +// font-size: 28rpx; | ||
| 492 | +// } | ||
| 493 | + | ||
| 494 | +// .text-base { | ||
| 495 | +// font-size: 32rpx; | ||
| 496 | +// line-height: 1.5; | ||
| 497 | +// } | ||
| 498 | + | ||
| 499 | +// .text-gray-500 { | ||
| 500 | +// color: #6b7280; | ||
| 501 | +// } | ||
| 502 | + | ||
| 503 | +// .block { | ||
| 504 | +// display: block; | ||
| 505 | +// } | ||
| 506 | + | ||
| 507 | +// NutUI 组件样式覆盖 | ||
| 508 | +:deep(.nut-textarea) { | ||
| 509 | + .nut-textarea__textarea { | ||
| 510 | + border: 1rpx solid #e5e7eb; | ||
| 511 | + border-radius: 12rpx; | ||
| 512 | + padding: 16rpx; | ||
| 513 | + font-size: 30rpx; | ||
| 514 | + line-height: 1.5; | ||
| 515 | + } | ||
| 516 | +} | ||
| 517 | + | ||
| 518 | +:deep(.nut-button) { | ||
| 519 | + border-radius: 12rpx; | ||
| 520 | +} | ||
| 521 | +</style> |
| ... | @@ -40,8 +40,9 @@ | ... | @@ -40,8 +40,9 @@ |
| 40 | </nut-sticky> | 40 | </nut-sticky> |
| 41 | 41 | ||
| 42 | <!-- 消息列表内容 --> | 42 | <!-- 消息列表内容 --> |
| 43 | - <scroll-view ref="scrollViewRef" class="conversation-list" :style="scrollStyle" :scroll-y="true" @scrolltolower="loadMore" | 43 | + <scroll-view ref="scrollViewRef" class="conversation-list" :style="scrollStyle" :scroll-y="true" |
| 44 | - @scroll="scroll" :lower-threshold="100" :enable-flex="false" :scroll-top="scrollTop"> | 44 | + @scrolltolower="loadMore" @scroll="scroll" :lower-threshold="100" :enable-flex="false" |
| 45 | + :scroll-top="scrollTop"> | ||
| 45 | <view v-for="conversation in filteredConversations" :key="conversation.id" | 46 | <view v-for="conversation in filteredConversations" :key="conversation.id" |
| 46 | class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2" | 47 | class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2" |
| 47 | @click="onConversationClick(conversation)"> | 48 | @click="onConversationClick(conversation)"> |
| ... | @@ -70,7 +71,8 @@ | ... | @@ -70,7 +71,8 @@ |
| 70 | </view> | 71 | </view> |
| 71 | 72 | ||
| 72 | <!-- 空状态提示 --> | 73 | <!-- 空状态提示 --> |
| 73 | - <view v-if="filteredConversations.length === 0 && !loading && !hasMore" class="empty-state py-8 text-center"> | 74 | + <view v-if="filteredConversations.length === 0 && !loading && !hasMore" |
| 75 | + class="empty-state py-8 text-center"> | ||
| 74 | <Message size="48" color="#9ca3af" class="mb-4" /> | 76 | <Message size="48" color="#9ca3af" class="mb-4" /> |
| 75 | <text class="text-gray-500 text-base block mb-2">暂无消息</text> | 77 | <text class="text-gray-500 text-base block mb-2">暂无消息</text> |
| 76 | <text class="text-gray-400 text-sm">当前筛选条件下没有找到相关消息</text> | 78 | <text class="text-gray-400 text-sm">当前筛选条件下没有找到相关消息</text> |
| ... | @@ -91,39 +93,8 @@ | ... | @@ -91,39 +93,8 @@ |
| 91 | <TabBar /> | 93 | <TabBar /> |
| 92 | 94 | ||
| 93 | <!-- 消息详情弹框 --> | 95 | <!-- 消息详情弹框 --> |
| 94 | - <nut-popup v-model:visible="showMessageDetail" position="right" :style="{ width: '100%', height: '100%' }" | 96 | + <MessageDetail v-model="showMessageDetail" :conversation="selectedConversation" @close="closeMessageDetail" |
| 95 | - closeable close-icon-position="top-right" @close="closeMessageDetail"> | 97 | + @sendMessage="handleSendMessage" /> |
| 96 | - <view class="message-detail-container"> | ||
| 97 | - <!-- 详情页头部 --> | ||
| 98 | - <view class="detail-header"> | ||
| 99 | - <view class="flex items-center"> | ||
| 100 | - <image v-if="selectedConversation?.avatar" :src="selectedConversation.avatar" | ||
| 101 | - class="w-12 h-12 rounded-full object-cover mr-3" mode="aspectFill" /> | ||
| 102 | - <view v-else class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center mr-3"> | ||
| 103 | - <component :is="selectedConversation?.icon" /> | ||
| 104 | - </view> | ||
| 105 | - <view class="flex-1"> | ||
| 106 | - <text class="text-lg font-medium">{{ selectedConversation?.name }}</text> | ||
| 107 | - <text class="text-sm text-gray-500 block">{{ selectedConversation?.time }}</text> | ||
| 108 | - </view> | ||
| 109 | - </view> | ||
| 110 | - </view> | ||
| 111 | - | ||
| 112 | - <!-- 消息内容 --> | ||
| 113 | - <view class="detail-content"> | ||
| 114 | - <view class="message-content"> | ||
| 115 | - <text class="text-base">{{ selectedConversation?.lastMessage }}</text> | ||
| 116 | - </view> | ||
| 117 | - </view> | ||
| 118 | - | ||
| 119 | - <!-- 底部关闭按钮 --> | ||
| 120 | - <view class="detail-footer"> | ||
| 121 | - <nut-button type="primary" size="large" block @click="closeMessageDetail" color="orange"> | ||
| 122 | - 关闭 | ||
| 123 | - </nut-button> | ||
| 124 | - </view> | ||
| 125 | - </view> | ||
| 126 | - </nut-popup> | ||
| 127 | </view> | 98 | </view> |
| 128 | </template> | 99 | </template> |
| 129 | 100 | ||
| ... | @@ -131,10 +102,12 @@ | ... | @@ -131,10 +102,12 @@ |
| 131 | import { ref, computed, onMounted, markRaw } from 'vue' | 102 | import { ref, computed, onMounted, markRaw } from 'vue' |
| 132 | import { Search2, Notice, Message } from '@nutui/icons-vue-taro' | 103 | import { Search2, Notice, Message } from '@nutui/icons-vue-taro' |
| 133 | import TabBar from '@/components/TabBar.vue' | 104 | import TabBar from '@/components/TabBar.vue' |
| 105 | +import MessageDetail from '@/components/MessageDetail.vue' | ||
| 134 | import { $ } from '@tarojs/extend' | 106 | import { $ } from '@tarojs/extend' |
| 107 | +import Taro from '@tarojs/taro' | ||
| 135 | 108 | ||
| 136 | // 默认头像 | 109 | // 默认头像 |
| 137 | -const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | 110 | +// const defaultAvatar = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' |
| 138 | 111 | ||
| 139 | const scrollStyle = ref({ | 112 | const scrollStyle = ref({ |
| 140 | height: 'calc(100vh - 500rpx)' | 113 | height: 'calc(100vh - 500rpx)' |
| ... | @@ -329,6 +302,25 @@ const markAsRead = (conversationId) => { | ... | @@ -329,6 +302,25 @@ const markAsRead = (conversationId) => { |
| 329 | } | 302 | } |
| 330 | } | 303 | } |
| 331 | 304 | ||
| 305 | +// 处理发送消息 | ||
| 306 | +const handleSendMessage = (data) => { | ||
| 307 | + const { conversation, message } = data | ||
| 308 | + if (!conversation || !message.trim()) return | ||
| 309 | + | ||
| 310 | + // 更新对话的最后一条消息 | ||
| 311 | + const conv = conversations.value.find(conv => conv.id === conversation.id) | ||
| 312 | + if (conv) { | ||
| 313 | + conv.lastMessage = message | ||
| 314 | + conv.time = '刚刚' | ||
| 315 | + conv.unread = false // 标记为已读 | ||
| 316 | + } | ||
| 317 | + | ||
| 318 | + Taro.showToast({ | ||
| 319 | + title: '消息发送成功', | ||
| 320 | + icon: 'success' | ||
| 321 | + }) | ||
| 322 | +} | ||
| 323 | + | ||
| 332 | // 页面加载时初始化数据 | 324 | // 页面加载时初始化数据 |
| 333 | onMounted(() => { | 325 | onMounted(() => { |
| 334 | // 设置滚动列表可视高度 | 326 | // 设置滚动列表可视高度 |
| ... | @@ -560,69 +552,7 @@ onMounted(() => { | ... | @@ -560,69 +552,7 @@ onMounted(() => { |
| 560 | position: relative; | 552 | position: relative; |
| 561 | } | 553 | } |
| 562 | 554 | ||
| 563 | - /* 消息详情弹框样式 */ | ||
| 564 | - .message-detail-container { | ||
| 565 | - height: 100%; | ||
| 566 | - display: flex; | ||
| 567 | - flex-direction: column; | ||
| 568 | - background: #ffffff; | ||
| 569 | - } | ||
| 570 | 555 | ||
| 571 | - .detail-header { | ||
| 572 | - padding: 32rpx 24rpx; | ||
| 573 | - border-bottom: 1rpx solid #f0f0f0; | ||
| 574 | - background: #ffffff; | ||
| 575 | - flex-shrink: 0; | ||
| 576 | - } | ||
| 577 | - | ||
| 578 | - .detail-content { | ||
| 579 | - flex: 1; | ||
| 580 | - padding: 24rpx; | ||
| 581 | - overflow-y: auto; | ||
| 582 | - background: #f8f9fa; | ||
| 583 | - } | ||
| 584 | - | ||
| 585 | - .message-content { | ||
| 586 | - background: #ffffff; | ||
| 587 | - padding: 24rpx; | ||
| 588 | - border-radius: 16rpx; | ||
| 589 | - margin-bottom: 24rpx; | ||
| 590 | - box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); | ||
| 591 | - } | ||
| 592 | - | ||
| 593 | - .message-history { | ||
| 594 | - .message-item { | ||
| 595 | - background: #ffffff; | ||
| 596 | - padding: 20rpx; | ||
| 597 | - border-radius: 12rpx; | ||
| 598 | - margin-bottom: 16rpx; | ||
| 599 | - box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.05); | ||
| 600 | - } | ||
| 601 | - } | ||
| 602 | - | ||
| 603 | - .detail-footer { | ||
| 604 | - padding: 24rpx; | ||
| 605 | - background: #ffffff; | ||
| 606 | - border-top: 1rpx solid #f0f0f0; | ||
| 607 | - flex-shrink: 0; | ||
| 608 | - } | ||
| 609 | - | ||
| 610 | - .text-lg { | ||
| 611 | - font-size: 36rpx; | ||
| 612 | - } | ||
| 613 | - | ||
| 614 | - .text-base { | ||
| 615 | - font-size: 32rpx; | ||
| 616 | - line-height: 1.5; | ||
| 617 | - } | ||
| 618 | - | ||
| 619 | - .mr-3 { | ||
| 620 | - margin-right: 24rpx; | ||
| 621 | - } | ||
| 622 | - | ||
| 623 | - .mt-1 { | ||
| 624 | - margin-top: 8rpx; | ||
| 625 | - } | ||
| 626 | 556 | ||
| 627 | } | 557 | } |
| 628 | 558 | ||
| ... | @@ -659,7 +589,8 @@ onMounted(() => { | ... | @@ -659,7 +589,8 @@ onMounted(() => { |
| 659 | /* 状态筛选标签 */ | 589 | /* 状态筛选标签 */ |
| 660 | .status-tabs { | 590 | .status-tabs { |
| 661 | background: white; | 591 | background: white; |
| 662 | - padding: 20rpx 35rpx; /* 增加内边距 */ | 592 | + padding: 20rpx 35rpx; |
| 593 | + /* 增加内边距 */ | ||
| 663 | border-bottom: 1rpx solid #e5e7eb; | 594 | border-bottom: 1rpx solid #e5e7eb; |
| 664 | display: flex; | 595 | display: flex; |
| 665 | position: relative; | 596 | position: relative; |
| ... | @@ -668,14 +599,15 @@ onMounted(() => { | ... | @@ -668,14 +599,15 @@ onMounted(() => { |
| 668 | -ms-overflow-style: none; | 599 | -ms-overflow-style: none; |
| 669 | 600 | ||
| 670 | &::-webkit-scrollbar { | 601 | &::-webkit-scrollbar { |
| 671 | - display: none; | 602 | + display: none; |
| 672 | } | 603 | } |
| 673 | } | 604 | } |
| 674 | 605 | ||
| 675 | .tab-item { | 606 | .tab-item { |
| 676 | margin-right: 48rpx; | 607 | margin-right: 48rpx; |
| 677 | padding-bottom: 16rpx; | 608 | padding-bottom: 16rpx; |
| 678 | - font-size: 30rpx; /* 增大字体 */ | 609 | + font-size: 30rpx; |
| 610 | + /* 增大字体 */ | ||
| 679 | color: #6b7280; | 611 | color: #6b7280; |
| 680 | position: relative; | 612 | position: relative; |
| 681 | cursor: pointer; | 613 | cursor: pointer; |
| ... | @@ -685,30 +617,30 @@ onMounted(() => { | ... | @@ -685,30 +617,30 @@ onMounted(() => { |
| 685 | transform: translateX(0); | 617 | transform: translateX(0); |
| 686 | 618 | ||
| 687 | &.active { | 619 | &.active { |
| 688 | - color: #f97316; | 620 | + color: #f97316; |
| 689 | - font-weight: 500; | 621 | + font-weight: 500; |
| 690 | - transform: translateY(-2rpx); | 622 | + transform: translateY(-2rpx); |
| 691 | - | 623 | + |
| 692 | - &::after { | 624 | + &::after { |
| 693 | - content: ''; | 625 | + content: ''; |
| 694 | - position: absolute; | 626 | + position: absolute; |
| 695 | - bottom: 0; | 627 | + bottom: 0; |
| 696 | - left: 0; | 628 | + left: 0; |
| 697 | - right: 0; | 629 | + right: 0; |
| 698 | - height: 4rpx; | 630 | + height: 4rpx; |
| 699 | - background: linear-gradient(90deg, #f97316, #fb923c); | 631 | + background: linear-gradient(90deg, #f97316, #fb923c); |
| 700 | - border-radius: 2rpx; | 632 | + border-radius: 2rpx; |
| 701 | - animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); | 633 | + animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| 702 | - } | 634 | + } |
| 703 | } | 635 | } |
| 704 | 636 | ||
| 705 | &:hover { | 637 | &:hover { |
| 706 | - color: #f97316; | 638 | + color: #f97316; |
| 707 | - transform: translateY(-1rpx); | 639 | + transform: translateY(-1rpx); |
| 708 | } | 640 | } |
| 709 | 641 | ||
| 710 | &:last-child { | 642 | &:last-child { |
| 711 | - margin-right: 0; | 643 | + margin-right: 0; |
| 712 | } | 644 | } |
| 713 | } | 645 | } |
| 714 | 646 | ||
| ... | @@ -720,6 +652,7 @@ onMounted(() => { | ... | @@ -720,6 +652,7 @@ onMounted(() => { |
| 720 | transform: scaleX(0); | 652 | transform: scaleX(0); |
| 721 | opacity: 0; | 653 | opacity: 0; |
| 722 | } | 654 | } |
| 655 | + | ||
| 723 | 100% { | 656 | 100% { |
| 724 | transform: scaleX(1); | 657 | transform: scaleX(1); |
| 725 | opacity: 1; | 658 | opacity: 1; |
| ... | @@ -737,6 +670,7 @@ onMounted(() => { | ... | @@ -737,6 +670,7 @@ onMounted(() => { |
| 737 | opacity: 0; | 670 | opacity: 0; |
| 738 | transform: translateY(20rpx); | 671 | transform: translateY(20rpx); |
| 739 | } | 672 | } |
| 673 | + | ||
| 740 | to { | 674 | to { |
| 741 | opacity: 1; | 675 | opacity: 1; |
| 742 | transform: translateY(0); | 676 | transform: translateY(0); |
| ... | @@ -749,17 +683,32 @@ onMounted(() => { | ... | @@ -749,17 +683,32 @@ onMounted(() => { |
| 749 | animation-fill-mode: both; | 683 | animation-fill-mode: both; |
| 750 | } | 684 | } |
| 751 | 685 | ||
| 752 | -.conversation-item:nth-child(1) { animation-delay: 0.1s; } | 686 | +.conversation-item:nth-child(1) { |
| 753 | -.conversation-item:nth-child(2) { animation-delay: 0.15s; } | 687 | + animation-delay: 0.1s; |
| 754 | -.conversation-item:nth-child(3) { animation-delay: 0.2s; } | 688 | +} |
| 755 | -.conversation-item:nth-child(4) { animation-delay: 0.25s; } | 689 | + |
| 756 | -.conversation-item:nth-child(5) { animation-delay: 0.3s; } | 690 | +.conversation-item:nth-child(2) { |
| 691 | + animation-delay: 0.15s; | ||
| 692 | +} | ||
| 693 | + | ||
| 694 | +.conversation-item:nth-child(3) { | ||
| 695 | + animation-delay: 0.2s; | ||
| 696 | +} | ||
| 697 | + | ||
| 698 | +.conversation-item:nth-child(4) { | ||
| 699 | + animation-delay: 0.25s; | ||
| 700 | +} | ||
| 701 | + | ||
| 702 | +.conversation-item:nth-child(5) { | ||
| 703 | + animation-delay: 0.3s; | ||
| 704 | +} | ||
| 757 | 705 | ||
| 758 | @keyframes fadeInItem { | 706 | @keyframes fadeInItem { |
| 759 | from { | 707 | from { |
| 760 | opacity: 0; | 708 | opacity: 0; |
| 761 | transform: translateX(-20rpx); | 709 | transform: translateX(-20rpx); |
| 762 | } | 710 | } |
| 711 | + | ||
| 763 | to { | 712 | to { |
| 764 | opacity: 1; | 713 | opacity: 1; |
| 765 | transform: translateX(0); | 714 | transform: translateX(0); | ... | ... |
-
Please register or login to post a comment