im-ui-mobile 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/components/im-avatar/im-avatar.vue +7 -7
  2. package/components/im-badge/im-badge.vue +326 -0
  3. package/components/im-button/im-button.vue +71 -34
  4. package/components/im-card/im-card.vue +563 -0
  5. package/components/im-chat-item/im-chat-item.vue +5 -4
  6. package/components/im-col/im-col.vue +191 -0
  7. package/components/im-dialog/im-dialog.vue +543 -0
  8. package/components/im-double-tap-view/im-double-tap-view.vue +93 -0
  9. package/components/im-emoji-picker/im-emoji-picker.vue +1143 -0
  10. package/components/im-friend-item/im-friend-item.vue +1 -1
  11. package/components/im-group-item/im-group-item.vue +1 -1
  12. package/components/im-group-member-selector/im-group-member-selector.vue +5 -5
  13. package/components/im-group-rtc-join/im-group-rtc-join.vue +8 -8
  14. package/components/im-icon/im-icon.vue +593 -0
  15. package/components/im-image-upload/im-image-upload.vue +0 -2
  16. package/components/im-link/im-link.vue +628 -0
  17. package/components/im-loading/im-loading.vue +13 -4
  18. package/components/im-mention-picker/im-mention-picker.vue +8 -7
  19. package/components/im-message-action/im-message-action.vue +678 -0
  20. package/components/im-message-item/im-message-item.vue +28 -26
  21. package/components/im-message-list/im-message-list.vue +1108 -0
  22. package/components/im-modal/im-modal.vue +373 -0
  23. package/components/im-nav-bar/im-nav-bar.vue +689 -75
  24. package/components/im-parse/im-parse.vue +1054 -0
  25. package/components/im-popup/im-popup.vue +467 -0
  26. package/components/im-read-receipt/im-read-receipt.vue +10 -10
  27. package/components/im-row/im-row.vue +189 -0
  28. package/components/im-search/im-search.vue +762 -0
  29. package/components/im-sku/im-sku.vue +720 -0
  30. package/components/im-sku/utils/helper.ts +182 -0
  31. package/components/im-stepper/im-stepper.vue +585 -0
  32. package/components/im-stepper/utils/helper.ts +167 -0
  33. package/components/im-tabs/im-tabs.vue +1022 -0
  34. package/components/im-tabs/tabs-navigation.vue +489 -0
  35. package/components/im-tabs/utils/helper.ts +181 -0
  36. package/components/im-tabs-tab-pane/im-tabs-tab-pane.vue +145 -0
  37. package/components/im-upload/im-upload.vue +1236 -0
  38. package/components/im-voice-input/im-voice-input.vue +1 -1
  39. package/index.js +3 -5
  40. package/index.scss +19 -0
  41. package/libs/emoji-data.ts +229 -0
  42. package/libs/index.ts +16 -16
  43. package/package.json +1 -2
  44. package/styles/button.scss +33 -33
  45. package/theme.scss +2 -2
  46. package/types/components/badge.d.ts +42 -0
  47. package/types/components/button.d.ts +2 -1
  48. package/types/components/card.d.ts +122 -0
  49. package/types/components/col.d.ts +37 -0
  50. package/types/components/dialog.d.ts +125 -0
  51. package/types/components/double-tap-view.d.ts +31 -0
  52. package/types/components/emoji-picker.d.ts +121 -0
  53. package/types/components/group-rtc-join.d.ts +1 -1
  54. package/types/components/icon.d.ts +77 -0
  55. package/types/components/link.d.ts +55 -0
  56. package/types/components/loading.d.ts +1 -0
  57. package/types/components/message-action.d.ts +96 -0
  58. package/types/components/message-item.d.ts +2 -2
  59. package/types/components/message-list.d.ts +136 -0
  60. package/types/components/modal.d.ts +106 -0
  61. package/types/components/nav-bar.d.ts +125 -0
  62. package/types/components/parse.d.ts +90 -0
  63. package/types/components/popup.d.ts +58 -0
  64. package/types/components/row.d.ts +31 -0
  65. package/types/components/search.d.ts +54 -0
  66. package/types/components/sku.d.ts +195 -0
  67. package/types/components/stepper.d.ts +99 -0
  68. package/types/components/tabs-tab-pane.d.ts +27 -0
  69. package/types/components/tabs.d.ts +117 -0
  70. package/types/components/upload.d.ts +137 -0
  71. package/types/components.d.ts +19 -1
  72. package/types/index.d.ts +38 -1
  73. package/types/libs/index.d.ts +10 -10
  74. package/types/utils/base64.d.ts +5 -0
  75. package/types/utils/dom.d.ts +3 -0
  76. package/types/utils/enums.d.ts +4 -5
  77. package/types/utils/validator.d.ts +74 -0
  78. package/utils/base64.js +18 -0
  79. package/utils/dom.js +353 -1
  80. package/utils/enums.js +4 -5
  81. package/utils/validator.js +230 -0
  82. package/components/im-file-upload/im-file-upload.vue +0 -309
  83. package/plugins/uview-plus.js +0 -29
  84. package/types/components/arrow-bar.d.ts +0 -14
  85. package/types/components/file-upload.d.ts +0 -58
@@ -0,0 +1,1108 @@
1
+ <template>
2
+ <view class="im-message-list" :class="[
3
+ `im-message-list--${theme}`,
4
+ `im-message-list--${mode}`,
5
+ {
6
+ 'im-message-list--empty': isEmpty,
7
+ 'im-message-list--unread': showUnreadTips,
8
+ 'im-message-list--typing': showTyping
9
+ }
10
+ ]" :style="listStyle" @touchmove="handleTouchMove" @touchend="handleTouchEnd">
11
+ <!-- 加载更多指示器 -->
12
+ <im-loading v-if="loading" :text="loadingText" />
13
+ <!-- <view v-if="loading && loadingPosition === 'top'" class="im-message-list__loading-top">
14
+ <im-loading :text="loadingText" />
15
+ </view> -->
16
+
17
+ <!-- 未读消息提示 -->
18
+ <view v-if="showUnreadTips" class="im-message-list__unread-tips" @click="scrollToUnread">
19
+ <im-icon name="arrow-down" size="16" />
20
+ <text class="unread-text">{{ unreadCount }} 条新消息</text>
21
+ </view>
22
+
23
+ <!-- 消息列表容器 -->
24
+ <scroll-view ref="scrollView" class="im-message-list__scroll-view" scroll-y :scroll-top="scrollTop"
25
+ :scroll-into-view="scrollIntoView" :scroll-with-animation="scrollAnimation"
26
+ :upper-threshold="loadMoreThreshold" :lower-threshold="scrollToBottomThreshold"
27
+ :show-scrollbar="showScrollbar" :enable-back-to-top="enableBackToTop" @scroll="handleScroll"
28
+ @scrolltoupper="handleScrollToUpper" @scrolltolower="handleScrollToLower">
29
+ <!-- 列表头部(用于加载更多) -->
30
+ <!-- <view v-if="loadingPosition === 'inside'" class="im-message-list__loading-inside">
31
+ <im-loading v-if="loading" :text="loadingText" />
32
+ </view> -->
33
+
34
+ <!-- 消息列表内容 -->
35
+ <view class="im-message-list__content">
36
+ <!-- 日期分隔符 -->
37
+ <!-- <view v-if="showDateDivider" v-for="(dateGroup, dateIndex) in groupedMessages"
38
+ :key="`date-${dateGroup.date}`" class="im-message-list__date-divider"
39
+ :id="`date-${dateGroup.date}`">
40
+ <text class="date-text">{{ formatDate(dateGroup.date) }}</text>
41
+ </view> -->
42
+ <view v-if="showUnreadTips" style="height: 80rpx;"></view>
43
+
44
+ <!-- 消息项 -->
45
+ <template v-for="(message, index) in processedMessages" :key="getMessageKey(message, index)">
46
+ <!-- 时间戳分隔(可选) -->
47
+ <view v-if="showTimestamp && shouldShowTimestamp(message, index)"
48
+ class="im-message-list__timestamp">
49
+ <text class="timestamp-text">{{ datetime.toTimeText(message.sendTime) }}</text>
50
+ </view>
51
+
52
+ <!-- 消息气泡 -->
53
+ <slot name="message" :message="message" :index="index">
54
+ <im-message-item :ref="setMessageRef(message.id)" :id="`msg-${index}`" :msgInfo="message"
55
+ :displayName="String(message.sendNickName)" :showTime="showMessageTime"
56
+ :showStatus="showMessageStatus && message.selfSend" :showMenu="showMessageMenu"
57
+ :showReceipt="showMessageReceipt && message.receipt" :groupMembers="groupMembers"
58
+ :customStyle="getMessageStyle(message)" @click="handleMessageClick(message, $event)"
59
+ @longpressAvatar="handleLongpressAvatar(message, $event)"
60
+ @longpress="handleLongpressMessage(message, $event)"
61
+ @clickAvatar="handleAvatarClick(message, $event)"
62
+ @click-name="handleNameClick(message, $event)" @resend="handleMessageResend(message)"
63
+ @recall="handleMessageRecall(message)" @delete="handleMessageDelete(message)"
64
+ @copy="handleMessageCopy(message)" @forward="handleMessageForward(message)"
65
+ @reply="handleMessageReply(message)" @download="handleMessageDownload(message)"
66
+ @audio-state-change="handleAudioPlay(message)" />
67
+ </slot>
68
+
69
+ <!-- 消息发送状态 -->
70
+ <!-- <view v-if="showMessageStatus && message.selfSend && message.status" class="im-message-list__status"
71
+ :class="`im-message-list__status--${message.status}`">
72
+ <im-icon v-if="getStatusIcons(message.status)" :name="getStatusIcons(message.status)" :size="14"
73
+ :class="{ 'spin-icon': message.status === MESSAGE_STATUS.SENDING }" />
74
+ <text v-else class="status-text">{{ getStatusText(message.status) || message.status }}></text>
75
+ </view> -->
76
+ </template>
77
+
78
+ <!-- 空状态 -->
79
+ <view v-if="isEmpty" class="im-message-list__empty">
80
+ <slot name="empty">
81
+ <im-icon name="message" size="48" :color="emptyIconColor" />
82
+ <text class="empty-text">{{ emptyText }}</text>
83
+ </slot>
84
+ </view>
85
+
86
+ <!-- 正在输入指示器 -->
87
+ <view v-if="showTyping" class="im-message-list__typing">
88
+ <slot name="typing">
89
+ <view class="typing-indicator">
90
+ <im-avatar :url="typingUser?.avatar" :name="typingUser?.name" size="small" />
91
+ <view class="typing-dots">
92
+ <view class="typing-dot" v-for="i in 3" :key="i"></view>
93
+ </view>
94
+ <text class="typing-text">{{ typingUser?.name || '对方' }}正在输入...</text>
95
+ </view>
96
+ </slot>
97
+ </view>
98
+ </view>
99
+
100
+ <!-- 列表底部(用于填充) -->
101
+ <view class="im-message-list__bottom-spacer" :style="{ height: bottomSpacerHeight + 'px' }"></view>
102
+ </scroll-view>
103
+
104
+ <!-- 滚动到底部按钮 -->
105
+ <view v-if="showScrollToBottom && !isAtBottom" class="im-message-list__scroll-bottom" @click="scrollToBottom">
106
+ <im-icon name="arrow-down" size="20" />
107
+ <text v-if="newMessageCount > 0" class="new-message-count">{{ newMessageCount }}</text>
108
+ </view>
109
+
110
+ <!-- 消息操作菜单 -->
111
+ <im-message-action v-if="showActionMenu" ref="actionMenu" :visible="actionMenuVisible"
112
+ :message="selectedMessage" :position="actionMenuPosition" :actions="actionMenuActions"
113
+ @close="closeActionMenu" @select="handleActionMenuSelect" />
114
+ </view>
115
+ </template>
116
+
117
+ <script setup lang="ts">
118
+ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
119
+ import { useDynamicRefs, datetime } from 'im-ui-mobile'
120
+ import { MESSAGE_TYPE, MESSAGE_STATUS } from '../..'
121
+ import { Message, GroupMember } from '../../libs'
122
+
123
+ // 使用动态引用
124
+ const { setRef: setMessageRef, getRef: getMessageRef } = useDynamicRefs();
125
+
126
+ // 类型定义
127
+ interface TypingUser {
128
+ id: number
129
+ name: string
130
+ avatar?: string
131
+ }
132
+
133
+ interface ActionMenuAction {
134
+ key: string
135
+ text: string
136
+ icon?: string
137
+ color?: string
138
+ disabled?: boolean
139
+ }
140
+
141
+ interface Props {
142
+ // 数据相关
143
+ messages: Message[]
144
+ groupMembers?: GroupMember[]
145
+ typingUser?: TypingUser | null
146
+
147
+ // 显示控制
148
+ theme?: 'light' | 'dark'
149
+ mode?: 'normal' | 'compact'
150
+ loading?: boolean
151
+ loadingText?: string
152
+ loadingPosition?: 'top' | 'inside'
153
+
154
+ // 日期时间显示
155
+ // showDateDivider?: boolean
156
+ showTimestamp?: boolean
157
+ timestampInterval?: number // 时间戳显示间隔(分钟)
158
+ showMessageTime?: boolean
159
+ dateFormat?: string
160
+ timeFormat?: string
161
+
162
+ // 消息显示
163
+ showMessageStatus?: boolean
164
+ showMessageMenu?: boolean
165
+ showMessageReceipt?: boolean
166
+ displayName?: string
167
+ showAvatar?: boolean
168
+ groupShowName?: boolean // 群聊是否显示名称
169
+
170
+ // 滚动相关
171
+ autoScroll?: boolean
172
+ scrollAnimation?: boolean
173
+ showScrollbar?: boolean
174
+ enableBackToTop?: boolean
175
+ loadMoreThreshold?: number
176
+ scrollToBottomThreshold?: number
177
+ keepAtBottom?: boolean // 新消息时保持底部
178
+
179
+ // 交互提示
180
+ showScrollToBottom?: boolean
181
+ showUnreadTips?: boolean
182
+ unreadCount?: number
183
+ newMessageCount?: number
184
+
185
+ // 空状态
186
+ emptyText?: string
187
+ emptyIconColor?: string
188
+
189
+ // 样式控制
190
+ backgroundColor?: string
191
+ minHeight?: string | number
192
+ maxHeight?: string | number
193
+ padding?: string | number
194
+ margin?: string | number
195
+
196
+ // 自定义操作
197
+ showActionMenu: boolean
198
+ actionMenuActions?: ActionMenuAction[]
199
+ }
200
+
201
+ const props = withDefaults(defineProps<Props>(), {
202
+ theme: 'light',
203
+ mode: 'normal',
204
+ loading: false,
205
+ loadingText: '加载中...',
206
+ loadingPosition: 'inside',
207
+
208
+ // showDateDivider: true,
209
+ showTimestamp: true,
210
+ timestampInterval: 5,
211
+ showMessageTime: false,
212
+ dateFormat: 'MM月dd日',
213
+ timeFormat: 'HH:mm',
214
+
215
+ showMessageStatus: true,
216
+ showMessageMenu: true,
217
+ showMessageReceipt: false,
218
+ displayName: '',
219
+ showAvatar: true,
220
+ groupShowName: true,
221
+
222
+ autoScroll: true,
223
+ scrollAnimation: true,
224
+ showScrollbar: true,
225
+ enableBackToTop: true,
226
+ loadMoreThreshold: 50,
227
+ scrollToBottomThreshold: 200,
228
+ keepAtBottom: true,
229
+
230
+ showScrollToBottom: true,
231
+ showUnreadTips: false,
232
+ unreadCount: 0,
233
+ newMessageCount: 0,
234
+
235
+ emptyText: '暂无消息',
236
+ emptyIconColor: '#C0C4CC',
237
+
238
+ showActionMenu: false,
239
+ actionMenuActions: () => [
240
+ { key: 'copy', text: '复制', icon: 'copy' },
241
+ { key: 'reply', text: '回复', icon: 'reply' },
242
+ { key: 'forward', text: '转发', icon: 'share' },
243
+ { key: 'recall', text: '撤回', icon: 'undo' },
244
+ { key: 'delete', text: '删除', icon: 'delete', color: '#F56C6C' }
245
+ ]
246
+ })
247
+
248
+ // Emits
249
+ const emit = defineEmits<{
250
+ // 数据事件
251
+ 'load-more': []
252
+ 'clickMessage': [message: Message, event: MouseEvent]
253
+ 'longpressMessage': [message: Message, event: TouchEvent]
254
+ 'longpressAvatar': [message: Message, event: TouchEvent]
255
+
256
+ // 消息操作
257
+ 'resend': [message: Message]
258
+ 'recall': [message: Message]
259
+ 'delete': [message: Message]
260
+ 'copy': [message: Message]
261
+ 'forward': [message: Message]
262
+ 'reply': [message: Message]
263
+ 'download': [message: Message]
264
+
265
+ // 交互事件
266
+ 'clickAvatar': [message: Message, event: MouseEvent]
267
+ 'click-name': [message: Message, event: MouseEvent]
268
+ 'scroll': [event: any]
269
+ 'scrollToBottom': []
270
+ 'scroll-to-top': []
271
+
272
+ // 音频事件
273
+ 'audio-play': [message: Message]
274
+ 'audio-pause': [message: Message]
275
+
276
+ // 动作菜单
277
+ 'action-menu-select': [action: ActionMenuAction, message: Message]
278
+ }>()
279
+
280
+ // 响应式数据
281
+ const scrollView = ref<any>(null)
282
+ const scrollTop = ref(0)
283
+ const scrollIntoView = ref('')
284
+ const isAtBottom = ref(true)
285
+ const isAtTop = ref(false)
286
+ const scrollDirection = ref<'up' | 'down'>('down')
287
+ const lastScrollTop = ref(0)
288
+ const actionMenuVisible = ref(false)
289
+ const selectedMessage = ref<Message | null>(null)
290
+ const actionMenuPosition = ref<'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'>('bottom')
291
+ const playingAudio = ref<Message | null>(null)
292
+ const touchStartY = ref(0)
293
+ const touchMoveY = ref(0)
294
+
295
+ // 计算属性
296
+ const isEmpty = computed(() => props.messages.length === 0)
297
+
298
+ const processedMessages = computed(() => {
299
+ return props.messages.filter(msg => {
300
+ // 过滤掉撤回的消息(如果需要可以配置化)
301
+ return msg.status !== MESSAGE_STATUS.RECALL
302
+ })
303
+ })
304
+
305
+ // const groupedMessages = computed(() => {
306
+ // if (!props.showDateDivider || isEmpty.value) return []
307
+
308
+ // const groups: Array<{ date: string; messages: Message[] }> = []
309
+ // let currentDate = ''
310
+ // let currentGroup: Message[] = []
311
+
312
+ // processedMessages.value.forEach((message, index) => {
313
+ // const messageDate = formatDate(message.sendTime, 'YYYY-MM-DD')
314
+
315
+ // if (messageDate !== currentDate) {
316
+ // if (currentGroup.length > 0) {
317
+ // groups.push({ date: currentDate, messages: [...currentGroup] })
318
+ // }
319
+ // currentDate = messageDate
320
+ // currentGroup = [message]
321
+ // } else {
322
+ // currentGroup.push(message)
323
+ // }
324
+
325
+ // // 处理最后一批
326
+ // if (index === processedMessages.value.length - 1) {
327
+ // groups.push({ date: currentDate, messages: currentGroup })
328
+ // }
329
+ // })
330
+
331
+ // return groups
332
+ // })
333
+
334
+ const showTyping = computed(() => !!props.typingUser)
335
+
336
+ const listStyle = computed(() => {
337
+ const style: Record<string, string> = {}
338
+
339
+ if (props.backgroundColor) {
340
+ style.backgroundColor = props.backgroundColor
341
+ }
342
+
343
+ if (props.minHeight) {
344
+ style.minHeight = typeof props.minHeight === 'number' ? `${props.minHeight}px` : props.minHeight
345
+ }
346
+
347
+ if (props.maxHeight) {
348
+ style.maxHeight = typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight
349
+ }
350
+
351
+ if (props.padding) {
352
+ style.padding = typeof props.padding === 'number' ? `${props.padding}px` : props.padding
353
+ }
354
+
355
+ if (props.margin) {
356
+ style.margin = typeof props.margin === 'number' ? `${props.margin}px` : props.margin
357
+ }
358
+
359
+ return style
360
+ })
361
+
362
+ const bottomSpacerHeight = computed(() => {
363
+ // 为键盘弹出留出空间
364
+ return showTyping.value ? 60 : 20
365
+ })
366
+
367
+ // const getStatusIcons = (status: MESSAGE_STATUS) => {
368
+ // switch (status) {
369
+ // case MESSAGE_STATUS.SENDING:
370
+ // return 'loading'
371
+ // case MESSAGE_STATUS.SENT:
372
+ // return 'ok'
373
+ // case MESSAGE_STATUS.RECEIVED:
374
+ // return 'ok'
375
+ // case MESSAGE_STATUS.READ:
376
+ // return 'ok'
377
+ // case MESSAGE_STATUS.FAILED:
378
+ // return 'close'
379
+ // }
380
+ // }
381
+
382
+ // const getStatusText = (status: MESSAGE_STATUS) => {
383
+ // switch (status) {
384
+ // case MESSAGE_STATUS.SENDING:
385
+ // return '发送中'
386
+ // case MESSAGE_STATUS.SENT:
387
+ // return '已发送'
388
+ // case MESSAGE_STATUS.RECEIVED:
389
+ // return '已送达'
390
+ // case MESSAGE_STATUS.READ:
391
+ // return '已读'
392
+ // case MESSAGE_STATUS.FAILED:
393
+ // return '发送失败'
394
+ // }
395
+ // }
396
+
397
+ // 方法
398
+ const getMessageKey = (message: Message, index: number) => {
399
+ return message.id ? `msg-${message.id}` : `msg-tmp-${index}-${message.sendTime}`
400
+ }
401
+
402
+ // const formatDate = (timestamp: any, format?: string): string => {
403
+ // const date = new Date(timestamp)
404
+ // const fmt = format || props.dateFormat
405
+
406
+ // if (fmt === 'MM月dd日') {
407
+ // return `${date.getMonth() + 1}月${date.getDate()}日`
408
+ // } else if (fmt === 'YYYY-MM-DD') {
409
+ // return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
410
+ // }
411
+
412
+ // // 简单实现,可以替换为更完整的日期格式化库
413
+ // return fmt
414
+ // .replace('YYYY', date.getFullYear().toString())
415
+ // .replace('MM', (date.getMonth() + 1).toString().padStart(2, '0'))
416
+ // .replace('dd', date.getDate().toString().padStart(2, '0'))
417
+ // .replace('M', (date.getMonth() + 1).toString())
418
+ // .replace('d', date.getDate().toString())
419
+ // }
420
+
421
+ // const formatTime = (timestamp: any): string => {
422
+ // const date = new Date(timestamp)
423
+ // const fmt = props.timeFormat
424
+
425
+ // return fmt
426
+ // .replace('HH', date.getHours().toString().padStart(2, '0'))
427
+ // .replace('mm', date.getMinutes().toString().padStart(2, '0'))
428
+ // .replace('H', date.getHours().toString())
429
+ // .replace('m', date.getMinutes().toString())
430
+ // }
431
+
432
+ const shouldShowTimestamp = (message: Message, index: number): boolean => {
433
+ if (index === 0) {
434
+ return (+new Date() - Number(message.sendTime)) > props.timestampInterval * 60 * 1000
435
+ }
436
+
437
+ if (!props.showTimestamp) {
438
+ return false
439
+ }
440
+
441
+ const prevMessage = processedMessages.value[index - 1]
442
+ const timeDiff = Number(message.sendTime) - Number(prevMessage.sendTime)
443
+
444
+ // 超过时间间隔显示时间戳
445
+ return timeDiff > props.timestampInterval * 60 * 1000
446
+ }
447
+
448
+ // const shouldShowName = (message: Message, index: number): boolean => {
449
+ // if (!props.displayName || message.selfSend) return false
450
+
451
+ // if (props.groupMembers && props.groupMembers.length > 0) {
452
+ // if (!props.groupShowName) return false
453
+
454
+ // // 群聊中,连续多条同一个人发的消息,只显示一次名字
455
+ // if (index > 0) {
456
+ // const prevMessage = processedMessages.value[index - 1]
457
+ // if (prevMessage.sendId === message.sendId) {
458
+ // const timeDiff = message.sendTime - prevMessage.sendTime
459
+ // // 10分钟内连续发送不显示名字
460
+ // if (timeDiff < 10 * 60 * 1000) {
461
+ // return false
462
+ // }
463
+ // }
464
+ // }
465
+ // return true
466
+ // }
467
+
468
+ // return false
469
+ // }
470
+
471
+ // const shouldShowAvatar = (message: Message, index: number): boolean => {
472
+ // if (!props.showAvatar) return false
473
+
474
+ // if (props.groupMembers && props.groupMembers.length > 0) {
475
+ // // 群聊中,连续多条同一个人发的消息,只显示一次头像
476
+ // if (index > 0) {
477
+ // const prevMessage = processedMessages.value[index - 1]
478
+ // if (prevMessage.sendId === message.sendId) {
479
+ // const timeDiff = Number(message.sendTime) - Number(prevMessage.sendTime)
480
+ // // 10分钟内连续发送不显示头像
481
+ // if (timeDiff < 10 * 60 * 1000) {
482
+ // return false
483
+ // }
484
+ // }
485
+ // }
486
+ // }
487
+
488
+ // return true
489
+ // }
490
+
491
+ const getMessageStyle = (message: Message): Record<string, any> => {
492
+ const style: Record<string, any> = {}
493
+
494
+ // 可以根据消息类型添加特殊样式
495
+ if (message.type === MESSAGE_TYPE.TIP_TEXT) {
496
+ style.opacity = 0.7
497
+ }
498
+
499
+ return style
500
+ }
501
+
502
+ const handleScroll = (event: any) => {
503
+ scrollIntoView.value = ''
504
+ const currentScrollTop = event.detail.scrollTop
505
+
506
+ // 判断滚动方向
507
+ scrollDirection.value = currentScrollTop > lastScrollTop.value ? 'down' : 'up'
508
+ lastScrollTop.value = currentScrollTop
509
+
510
+ // 判断是否在底部
511
+ const scrollHeight = event.detail.scrollHeight
512
+ const clientHeight = event.detail.clientHeight
513
+ // isAtBottom.value = scrollHeight - currentScrollTop - (clientHeight || 0) < 10
514
+
515
+ isAtBottom.value = false
516
+ // console.log('计算滚动 ', event.detail)
517
+
518
+ // 判断是否在顶部
519
+ isAtTop.value = currentScrollTop < 10
520
+
521
+ emit('scroll', event)
522
+ }
523
+
524
+ const handleScrollToUpper = () => {
525
+ if (props.loading) return
526
+
527
+ emit('load-more')
528
+ emit('scroll-to-top')
529
+ }
530
+
531
+ const handleScrollToLower = () => {
532
+ emit('scrollToBottom')
533
+ }
534
+
535
+ const scrollToBottom = () => {
536
+ nextTick(() => {
537
+ if (processedMessages.value.length > 0) {
538
+ const lastIndex = processedMessages.value.length - 1
539
+ scrollIntoView.value = `msg-${lastIndex}`
540
+ // scrollIntoView.value = `msg-${processedMessages.value[lastIndex].id || lastIndex}`
541
+ // console.log('scrollToBottom id', scrollIntoView.value)
542
+ }
543
+
544
+ // 如果是手动滚动到底部,更新状态
545
+ isAtBottom.value = true
546
+ emit('scrollToBottom')
547
+ })
548
+ }
549
+
550
+ const scrollToMessage = (messageId: number | string) => {
551
+ scrollIntoView.value = `msg-${messageId}`
552
+ }
553
+
554
+ const scrollToUnread = () => {
555
+ if (props.unreadCount && props.unreadCount > 0) {
556
+ // 滚动到第一条未读消息
557
+ const unreadIndex = processedMessages.value.findIndex(msg => msg.status !== MESSAGE_STATUS.READ && !msg.selfSend)
558
+ if (unreadIndex !== -1) {
559
+ scrollIntoView.value = `msg-${processedMessages.value[unreadIndex].id || unreadIndex}`
560
+ }
561
+ }
562
+ }
563
+
564
+ const handleTouchMove = (event: TouchEvent) => {
565
+ touchMoveY.value = event.touches[0].clientY
566
+ }
567
+
568
+ const handleTouchEnd = () => {
569
+ // 滑动结束处理
570
+ touchStartY.value = 0
571
+ touchMoveY.value = 0
572
+ }
573
+
574
+ // 消息事件处理
575
+ const handleMessageClick = (message: Message, event: MouseEvent) => {
576
+ emit('clickMessage', message, event)
577
+ }
578
+
579
+ const handleLongpressMessage = (message: Message, event: TouchEvent) => {
580
+ if (props.showMessageMenu) {
581
+ selectedMessage.value = message
582
+ // actionMenuPosition.value = {
583
+ // x: event.touches[0].clientX,
584
+ // y: event.touches[0].clientY
585
+ // }
586
+ actionMenuVisible.value = true
587
+ }
588
+
589
+ emit('longpressMessage', message, event)
590
+ }
591
+
592
+ const handleLongpressAvatar = (message: Message, event: TouchEvent) => {
593
+ emit('longpressAvatar', message, event)
594
+ }
595
+
596
+ const handleAvatarClick = (message: Message, event: MouseEvent) => {
597
+ emit('clickAvatar', message, event)
598
+ }
599
+
600
+ const handleNameClick = (message: Message, event: MouseEvent) => {
601
+ emit('click-name', message, event)
602
+ }
603
+
604
+ const handleMessageResend = (message: Message) => {
605
+ emit('resend', message)
606
+ }
607
+
608
+ const handleMessageRecall = (message: Message) => {
609
+ emit('recall', message)
610
+ }
611
+
612
+ const handleMessageDelete = (message: Message) => {
613
+ emit('delete', message)
614
+ }
615
+
616
+ const handleMessageCopy = (message: Message) => {
617
+ emit('copy', message)
618
+ }
619
+
620
+ const handleMessageForward = (message: Message) => {
621
+ emit('forward', message)
622
+ }
623
+
624
+ const handleMessageReply = (message: Message) => {
625
+ emit('reply', message)
626
+ }
627
+
628
+ const handleMessageDownload = (message: Message) => {
629
+ emit('download', message)
630
+ }
631
+
632
+ const handleAudioPlay = (message: Message) => {
633
+ // 停止当前播放的音频
634
+ if (playingAudio.value && playingAudio.value.id !== message.id) {
635
+ const audioRef = getMessageRef(playingAudio.value.id)?.[0]
636
+ audioRef?.stopAudio?.()
637
+ }
638
+
639
+ playingAudio.value = message
640
+ emit('audio-play', message)
641
+ }
642
+
643
+ // 操作菜单处理
644
+ const handleActionMenuSelect = (action: ActionMenuAction) => {
645
+ if (selectedMessage.value) {
646
+ emit('action-menu-select', action, selectedMessage.value)
647
+
648
+ // 执行对应操作
649
+ switch (action.key) {
650
+ case 'copy':
651
+ handleMessageCopy(selectedMessage.value)
652
+ break
653
+ case 'reply':
654
+ handleMessageReply(selectedMessage.value)
655
+ break
656
+ case 'forward':
657
+ handleMessageForward(selectedMessage.value)
658
+ break
659
+ case 'recall':
660
+ handleMessageRecall(selectedMessage.value)
661
+ break
662
+ case 'delete':
663
+ handleMessageDelete(selectedMessage.value)
664
+ break
665
+ }
666
+ }
667
+
668
+ closeActionMenu()
669
+ }
670
+
671
+ const closeActionMenu = () => {
672
+ actionMenuVisible.value = false
673
+ selectedMessage.value = null
674
+ }
675
+
676
+ // 监听消息变化,自动滚动到底部
677
+ watch(() => props.messages.length, (newLength, oldLength) => {
678
+ if (newLength > oldLength && props.autoScroll && (isAtBottom.value || props.keepAtBottom)) {
679
+ nextTick(() => {
680
+ scrollToBottom()
681
+ })
682
+ }
683
+ })
684
+
685
+ // 监听输入状态
686
+ watch(() => props.typingUser, (newTyping) => {
687
+ if (newTyping && isAtBottom.value) {
688
+ // 正在输入时保持滚动到底部
689
+ nextTick(() => {
690
+ scrollToBottom()
691
+ })
692
+ }
693
+ })
694
+
695
+ // 生命周期
696
+ onMounted(() => {
697
+ // 初始化滚动到底部
698
+ if (props.autoScroll && !isEmpty.value) {
699
+ nextTick(() => {
700
+ scrollToBottom()
701
+ })
702
+ }
703
+ })
704
+
705
+ onUnmounted(() => {
706
+ // 清理
707
+ if (playingAudio.value) {
708
+ const audioRef = getMessageRef(playingAudio.value.id)?.[0]
709
+ audioRef?.stopAudio?.()
710
+ }
711
+ })
712
+
713
+ // 暴露给父组件的方法
714
+ defineExpose({
715
+ scrollToBottom,
716
+ scrollToMessage,
717
+ scrollToUnread,
718
+ getScrollTop: () => scrollTop.value,
719
+ setScrollTop: (top: number) => {
720
+ scrollTop.value = top
721
+ },
722
+ stopAllAudio: () => {
723
+ if (playingAudio.value) {
724
+ const audioRef = getMessageRef(playingAudio.value.id)?.[0]
725
+ audioRef?.stopAudio?.()
726
+ playingAudio.value = null
727
+ }
728
+ }
729
+ })
730
+ </script>
731
+
732
+ <style scoped lang="scss">
733
+ .im-message-list {
734
+ position: relative;
735
+ height: 100%;
736
+ background-color: #f5f7fa;
737
+ overflow: hidden;
738
+ box-sizing: border-box;
739
+
740
+ // 主题变体
741
+ &--light {
742
+ background-color: #f5f7fa;
743
+ color: #333333;
744
+ }
745
+
746
+ &--dark {
747
+ background-color: #1a1a1a;
748
+ color: #ffffff;
749
+ }
750
+
751
+ &--compact {
752
+
753
+ .im-message-list__date-divider,
754
+ .im-message-list__timestamp {
755
+ margin: 8rpx 0;
756
+ }
757
+ }
758
+
759
+ // 状态类
760
+ &--loading {
761
+ opacity: 0.8;
762
+ }
763
+
764
+ &--empty {
765
+ display: flex;
766
+ align-items: center;
767
+ justify-content: center;
768
+ }
769
+
770
+ &--unread {
771
+ .im-message-list__unread-tips {
772
+ display: flex;
773
+ }
774
+ }
775
+
776
+ &--typing {
777
+ .im-message-list__typing {
778
+ display: block;
779
+ }
780
+ }
781
+ }
782
+
783
+ // 加载指示器
784
+ .im-message-list__loading-top,
785
+ .im-message-list__loading-inside {
786
+ display: flex;
787
+ align-items: center;
788
+ justify-content: center;
789
+ padding: 20rpx;
790
+ background-color: inherit;
791
+ }
792
+
793
+ .im-message-list__loading-top {
794
+ position: absolute;
795
+ top: 0;
796
+ left: 0;
797
+ right: 0;
798
+ z-index: 10;
799
+ border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
800
+
801
+ .im-message-list--dark & {
802
+ border-bottom-color: rgba(255, 255, 255, 0.1);
803
+ }
804
+ }
805
+
806
+ // 未读消息提示
807
+ .im-message-list__unread-tips {
808
+ position: absolute;
809
+ top: 10rpx;
810
+ left: 50%;
811
+ transform: translateX(-50%);
812
+ display: flex;
813
+ align-items: center;
814
+ background-color: #ffffff;
815
+ padding: 8rpx 24rpx;
816
+ border-radius: 20rpx;
817
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
818
+ z-index: 20;
819
+ cursor: pointer;
820
+
821
+ .im-message-list--dark & {
822
+ background-color: #2a2a2a;
823
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
824
+ }
825
+
826
+ .unread-text {
827
+ margin-left: 8rpx;
828
+ font-size: 24rpx;
829
+ color: #409eff;
830
+ font-weight: 500;
831
+ }
832
+ }
833
+
834
+ // 滚动视图
835
+ .im-message-list__scroll-view {
836
+ height: 100%;
837
+ box-sizing: border-box;
838
+ }
839
+
840
+ // 内容容器
841
+ .im-message-list__content {
842
+ padding: 20rpx;
843
+ box-sizing: border-box;
844
+ }
845
+
846
+ // 日期分隔符
847
+ .im-message-list__date-divider {
848
+ display: flex;
849
+ justify-content: center;
850
+ margin: 32rpx 0;
851
+ position: relative;
852
+
853
+ &::before,
854
+ &::after {
855
+ content: '';
856
+ position: absolute;
857
+ top: 50%;
858
+ width: 100rpx;
859
+ height: 1rpx;
860
+ background-color: rgba(0, 0, 0, 0.1);
861
+ }
862
+
863
+ &::before {
864
+ left: 50rpx;
865
+ }
866
+
867
+ &::after {
868
+ right: 50rpx;
869
+ }
870
+
871
+ .im-message-list--dark &::before,
872
+ .im-message-list--dark &::after {
873
+ background-color: rgba(255, 255, 255, 0.1);
874
+ }
875
+
876
+ .date-text {
877
+ font-size: 24rpx;
878
+ color: #909399;
879
+ background-color: inherit;
880
+ padding: 0 20rpx;
881
+ position: relative;
882
+ z-index: 1;
883
+
884
+ .im-message-list--dark & {
885
+ color: #aaaaaa;
886
+ }
887
+ }
888
+ }
889
+
890
+ // 时间戳
891
+ .im-message-list__timestamp {
892
+ display: flex;
893
+ justify-content: center;
894
+ margin-bottom: 40rpx;
895
+
896
+ .timestamp-text {
897
+ font-size: $im-font-size - 2;
898
+ color: $im-text-color-lighter;
899
+ // padding: 4rpx 16rpx;
900
+ // background-color: rgba(0, 0, 0, 0.05);
901
+ // border-radius: 12rpx;
902
+
903
+ .im-message-list--dark & {
904
+ color: #666666;
905
+ background-color: rgba(255, 255, 255, 0.1);
906
+ }
907
+ }
908
+ }
909
+
910
+ // 消息状态
911
+ .im-message-list__status {
912
+ display: flex;
913
+ align-items: center;
914
+ justify-content: flex-end;
915
+ margin-top: 4rpx;
916
+ padding-right: 80rpx;
917
+ font-size: 22rpx;
918
+
919
+ &--sending {
920
+ color: #909399;
921
+ }
922
+
923
+ &--sent,
924
+ &--received,
925
+ &--read {
926
+ color: #67c23a;
927
+ }
928
+
929
+ &--failed {
930
+ color: #f56c6c;
931
+ }
932
+
933
+ .spin-icon {
934
+ animation: spin 1s linear infinite;
935
+ }
936
+
937
+ .status-text {
938
+ margin-left: 4rpx;
939
+ }
940
+ }
941
+
942
+ // 空状态
943
+ .im-message-list__empty {
944
+ display: flex;
945
+ flex-direction: column;
946
+ align-items: center;
947
+ justify-content: center;
948
+ padding: 100rpx 0;
949
+ color: #c0c4cc;
950
+
951
+ .im-message-list--dark & {
952
+ color: #666666;
953
+ }
954
+
955
+ .empty-text {
956
+ margin-top: 20rpx;
957
+ font-size: 28rpx;
958
+ }
959
+ }
960
+
961
+ // 正在输入指示器
962
+ .im-message-list__typing {
963
+ margin: 20rpx 0;
964
+
965
+ .typing-indicator {
966
+ display: flex;
967
+ align-items: center;
968
+ padding: 16rpx 24rpx;
969
+ background-color: #ffffff;
970
+ border-radius: 20rpx;
971
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
972
+
973
+ .im-message-list--dark & {
974
+ background-color: #2a2a2a;
975
+ }
976
+ }
977
+
978
+ .typing-dots {
979
+ display: flex;
980
+ align-items: center;
981
+ margin: 0 16rpx;
982
+ }
983
+
984
+ .typing-dot {
985
+ width: 8rpx;
986
+ height: 8rpx;
987
+ border-radius: 50%;
988
+ background-color: #409eff;
989
+ margin: 0 4rpx;
990
+ animation: typing 1.4s infinite ease-in-out;
991
+
992
+ &:nth-child(1) {
993
+ animation-delay: -0.32s;
994
+ }
995
+
996
+ &:nth-child(2) {
997
+ animation-delay: -0.16s;
998
+ }
999
+ }
1000
+
1001
+ .typing-text {
1002
+ font-size: 24rpx;
1003
+ color: #606266;
1004
+
1005
+ .im-message-list--dark & {
1006
+ color: #cccccc;
1007
+ }
1008
+ }
1009
+ }
1010
+
1011
+ // 底部间距
1012
+ .im-message-list__bottom-spacer {
1013
+ width: 100%;
1014
+ }
1015
+
1016
+ // 滚动到底部按钮
1017
+ .im-message-list__scroll-bottom {
1018
+ position: absolute;
1019
+ right: 32rpx;
1020
+ bottom: 32rpx;
1021
+ width: 60rpx;
1022
+ height: 60rpx;
1023
+ display: flex;
1024
+ align-items: center;
1025
+ justify-content: center;
1026
+ background-color: #ffffff;
1027
+ border-radius: 50%;
1028
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
1029
+ cursor: pointer;
1030
+ z-index: 30;
1031
+ transition: all 0.3s ease;
1032
+
1033
+ &:active {
1034
+ transform: scale(0.95);
1035
+ }
1036
+
1037
+ .im-message-list--dark & {
1038
+ background-color: #2a2a2a;
1039
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
1040
+ }
1041
+
1042
+ .new-message-count {
1043
+ position: absolute;
1044
+ top: -8rpx;
1045
+ right: -8rpx;
1046
+ min-width: 32rpx;
1047
+ height: 32rpx;
1048
+ display: flex;
1049
+ align-items: center;
1050
+ justify-content: center;
1051
+ background-color: #f56c6c;
1052
+ color: #ffffff;
1053
+ font-size: 20rpx;
1054
+ border-radius: 16rpx;
1055
+ padding: 0 8rpx;
1056
+ }
1057
+ }
1058
+
1059
+ // 动画
1060
+ @keyframes spin {
1061
+ 0% {
1062
+ transform: rotate(0deg);
1063
+ }
1064
+
1065
+ 100% {
1066
+ transform: rotate(360deg);
1067
+ }
1068
+ }
1069
+
1070
+ @keyframes typing {
1071
+
1072
+ 0%,
1073
+ 80%,
1074
+ 100% {
1075
+ transform: scale(0.8);
1076
+ opacity: 0.5;
1077
+ }
1078
+
1079
+ 40% {
1080
+ transform: scale(1);
1081
+ opacity: 1;
1082
+ }
1083
+ }
1084
+
1085
+ // 响应式调整
1086
+ @media (max-width: 768px) {
1087
+ .im-message-list__content {
1088
+ padding: 16rpx;
1089
+ }
1090
+
1091
+ .im-message-list__date-divider {
1092
+ margin: 24rpx 0;
1093
+
1094
+ &::before,
1095
+ &::after {
1096
+ width: 60rpx;
1097
+ }
1098
+
1099
+ &::before {
1100
+ left: 32rpx;
1101
+ }
1102
+
1103
+ &::after {
1104
+ right: 32rpx;
1105
+ }
1106
+ }
1107
+ }
1108
+ </style>