im-ui-mobile 0.0.98 → 0.1.0

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 (40) hide show
  1. package/components/im-avatar/im-avatar.vue +121 -121
  2. package/components/im-button/im-button.vue +630 -626
  3. package/components/im-cell/im-cell.vue +10 -15
  4. package/components/im-cell-group/im-cell-group.vue +16 -16
  5. package/components/im-chat-item/im-chat-item.vue +213 -213
  6. package/components/im-context-menu/im-context-menu.vue +138 -138
  7. package/components/im-file-upload/im-file-upload.vue +309 -309
  8. package/components/im-friend-item/im-friend-item.vue +82 -75
  9. package/components/im-group-item/im-group-item.vue +62 -62
  10. package/components/im-group-member-selector/im-group-member-selector.vue +202 -202
  11. package/components/im-group-rtc-join/im-group-rtc-join.vue +112 -112
  12. package/components/im-image-upload/im-image-upload.vue +94 -94
  13. package/components/im-loading/im-loading.vue +64 -64
  14. package/components/im-mention-picker/im-mention-picker.vue +191 -191
  15. package/components/im-message-item/im-message-item.vue +555 -555
  16. package/components/im-nav-bar/im-nav-bar.vue +98 -98
  17. package/components/im-read-receipt/im-read-receipt.vue +174 -174
  18. package/components/im-virtual-list/im-virtual-list.vue +52 -51
  19. package/components/im-voice-input/im-voice-input.vue +305 -305
  20. package/libs/index.ts +2 -3
  21. package/package.json +58 -58
  22. package/styles/button.scss +1 -1
  23. package/theme.scss +61 -62
  24. package/types/components.d.ts +0 -1
  25. package/types/index.d.ts +94 -94
  26. package/types/libs/index.d.ts +206 -204
  27. package/types/utils/datetime.d.ts +9 -9
  28. package/types/utils/dom.d.ts +11 -11
  29. package/types/utils/emoji.d.ts +8 -8
  30. package/types/utils/enums.d.ts +73 -73
  31. package/types/utils/messageType.d.ts +35 -35
  32. package/types/utils/recorderApp.d.ts +9 -9
  33. package/types/utils/recorderH5.d.ts +9 -9
  34. package/types/utils/requester.d.ts +15 -15
  35. package/types/utils/url.d.ts +5 -5
  36. package/types/utils/useDynamicRefs.d.ts +9 -9
  37. package/types/utils/websocket.d.ts +34 -34
  38. package/utils/enums.js +1 -1
  39. package/components/im-virtual-scroller/im-virtual-scroller.vue +0 -54
  40. package/types/components/virtual-scroller.d.ts +0 -20
@@ -1,556 +1,556 @@
1
- <template>
2
- <view class="message-item">
3
- <view class="message-tip" v-if="msgInfo.type == MESSAGE_TYPE.TIP_TEXT">
4
- {{ msgInfo.content }}
5
- </view>
6
- <view class="message-tip" v-else-if="msgInfo.type == MESSAGE_TYPE.TIP_TIME">
7
- {{ $datetime.toTimeText(msgInfo.sendTime) }}
8
- </view>
9
- <view class="message-normal" v-else-if="isNormal" :class="{ 'message-mine': msgInfo.selfSend }">
10
- <im-avatar class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="avatar"
11
- :name="showName" size="small" />
12
- <view class="content">
13
- <view v-if="msgInfo.groupId && !msgInfo.selfSend" class="top">
14
- <text>{{ showName }}</text>
15
- </view>
16
- <view class="bottom">
17
- <view class="message-content-wrapper">
18
- <view v-if="msgInfo.type == MESSAGE_TYPE.TEXT">
19
- <im-context-menu :items="menuItems" @select="onSelectMenu">
20
- <!-- up-parse支持点击a标签,但是不支持显示emo表情,也不支持换行 -->
21
- <up-parse
22
- v-if="$url.containUrl(msgInfo.content) && !emoji.containEmoji(msgInfo.content)"
23
- class="message-text" :showImgMenu="false" :content="nodesText" />
24
- <!-- rich-text支持显示emo表情以及消息换行,但是不支持点击a标签 -->
25
- <rich-text v-else class="message-text" :nodes="nodesText" />
26
- </im-context-menu>
27
- </view>
28
- <view class="message-image" v-else-if="msgInfo.type == MESSAGE_TYPE.IMAGE">
29
- <im-context-menu :items="menuItems" @select="onSelectMenu">
30
- <view class="image-box">
31
- <image class="send-image" :style="imageStyle" mode="aspectFill"
32
- :src="contentData.thumbUrl" lazy-load="true" @click.stop="onShowFullImage()">
33
- </image>
34
- <im-loading v-if="sending"></im-loading>
35
- </view>
36
- </im-context-menu>
37
- </view>
38
- <view class="message-file" v-else-if="msgInfo.type == MESSAGE_TYPE.FILE">
39
- <im-context-menu :items="menuItems" @select="onSelectMenu">
40
- <view class="file-box">
41
- <view class="file-info" style="overflow: hidden;">
42
- <u-link class="file-name" :under-line="true" color="#007BFF"
43
- :href="contentData.url" :text="contentData.name"></u-link>
44
- <view class="file-size">{{ fileSize }}</view>
45
- </view>
46
- <view class="file-icon iconfont icon-file"></view>
47
- <im-loading v-if="sending"></im-loading>
48
- </view>
49
- </im-context-menu>
50
- </view>
51
- <im-context-menu v-else-if="msgInfo.type == MESSAGE_TYPE.AUDIO" :items="menuItems"
52
- @select="onSelectMenu">
53
- <view class="message-audio message-text" @click="onPlayAudio()">
54
- <text class="iconfont icon-voice-play"></text>
55
- <text class="chat-audio-text">{{ contentData.duration + '"' }}</text>
56
- <text v-if="audioPlayState == 'PAUSE'" class="iconfont icon-play"></text>
57
- <text v-if="audioPlayState == 'PLAYING'" class="iconfont icon-pause"></text>
58
- </view>
59
- </im-context-menu>
60
- <im-context-menu v-if="isAction" :items="menuItems" @select="onSelectMenu">
61
- <view class="chat-realtime message-text" @click="$emit('call')">
62
- <text v-if="msgInfo.type == MESSAGE_TYPE.ACT_RT_VOICE"
63
- class="iconfont icon-chat-voice"></text>
64
- <text v-if="msgInfo.type == MESSAGE_TYPE.ACT_RT_VIDEO"
65
- class="iconfont icon-chat-video"></text>
66
- <text>{{ msgInfo.content }}</text>
67
- </view>
68
- </im-context-menu>
69
- <view v-if="sending && isTextMessage" class="sending">
70
- <im-loading :size="40" icon-color="#656adf" :mask="false"></im-loading>
71
- </view>
72
- <view v-else-if="sendFail" @click="onSendFail"
73
- class="send-fail iconfont icon-warning-circle-fill"></view>
74
- </view>
75
- <view class="message-status" v-if="!isAction && msgInfo.selfSend && !msgInfo.groupId">
76
- <text class="chat-readed" v-if="msgInfo.status == MESSAGE_STATUS.READED">已读</text>
77
- <text class="chat-unread" v-else>未读</text>
78
- </view>
79
- <view class="chat-receipt" v-if="msgInfo.receipt" @click="onShowReadedBox">
80
- <text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text>
81
- <text v-else>{{ msgInfo.readedCount }}人已读</text>
82
- </view>
83
- </view>
84
- </view>
85
- </view>
86
- <im-read-receipt ref="chatGroupReaded" :groupMembers="groupMembers as any[]" :msgInfo="msgInfo" />
87
- </view>
88
- </template>
89
-
90
- <script setup lang="ts">
91
- import { ref, computed } from 'vue'
92
- import ImAvatar from '../im-avatar/im-avatar.vue'
93
- import ImReadReceipt from '../im-read-receipt/im-read-receipt.vue'
94
- import ImContextMenu from '../im-context-menu/im-context-menu.vue'
95
- import { MESSAGE_TYPE, MESSAGE_STATUS, datetime, dom, url, messageType, emoji } from '../../index'
96
-
97
- interface Props {
98
- avatar?: string;
99
- showName: string;
100
- msgInfo: any;
101
- groupMembers?: any[];
102
- }
103
-
104
- interface Emits {
105
- (e: 'call'): void;
106
- (e: 'longPressHead'): void;
107
- (e: 'resend', msgInfo: any): void;
108
- (e: 'audioStateChange', state: string, msgInfo: any): void;
109
- (e: 'copy', msgInfo: any): void;
110
- (e: 'recall', msgInfo: any): void;
111
- (e: 'delete', msgInfo: any): void;
112
- (e: 'download', msgInfo: any): void;
113
- }
114
-
115
- const $url = url
116
- const $datetime = datetime
117
- const props = defineProps<Props>();
118
- const emit = defineEmits<Emits>();
119
-
120
- const audioPlayState = ref('STOP');
121
- const innerAudioContext = ref<UniApp.InnerAudioContext | null>(null);
122
- const menu = ref({
123
- show: false,
124
- style: ""
125
- });
126
-
127
- const onSendFail = () => {
128
- emit("resend", props.msgInfo);
129
- };
130
-
131
- const onPlayAudio = () => {
132
- // 初始化音频播放器
133
- if (!innerAudioContext.value) {
134
- innerAudioContext.value = uni.createInnerAudioContext();
135
- const url = contentData.value.url;
136
- innerAudioContext.value.src = url;
137
- innerAudioContext.value.onEnded(() => {
138
- console.log('停止');
139
- audioPlayState.value = "STOP";
140
- emitAudioState();
141
- });
142
- innerAudioContext.value.onError(() => {
143
- audioPlayState.value = "STOP";
144
- console.log("播放音频出错");
145
- emitAudioState();
146
- });
147
- }
148
-
149
- if (audioPlayState.value === 'STOP') {
150
- innerAudioContext.value.play();
151
- audioPlayState.value = "PLAYING";
152
- } else if (audioPlayState.value === 'PLAYING') {
153
- innerAudioContext.value.pause();
154
- audioPlayState.value = "PAUSE";
155
- } else if (audioPlayState.value === 'PAUSE') {
156
- innerAudioContext.value.play();
157
- audioPlayState.value = "PLAYING";
158
- }
159
- emitAudioState();
160
- };
161
-
162
- const onSelectMenu = (item: any) => {
163
- emit(item.key.toLowerCase() as any, props.msgInfo);
164
- menu.value.show = false;
165
- };
166
-
167
- const onShowFullImage = () => {
168
- const imageUrl = contentData.value.originUrl;
169
- uni.previewImage({
170
- urls: [imageUrl]
171
- });
172
- };
173
-
174
- const onShowReadedBox = () => {
175
- // this.$refs.chatGroupReaded.open();
176
- };
177
-
178
- const emitAudioState = () => {
179
- emit("audioStateChange", audioPlayState.value, props.msgInfo);
180
- };
181
-
182
- const stopPlayAudio = () => {
183
- if (innerAudioContext.value) {
184
- innerAudioContext.value.stop();
185
- innerAudioContext.value = null;
186
- audioPlayState.value = "STOP";
187
- }
188
- };
189
-
190
- const sending = computed(() => {
191
- return props.msgInfo.status === MESSAGE_STATUS.SENDING;
192
- });
193
-
194
- const sendFail = computed(() => {
195
- return props.msgInfo.status === MESSAGE_STATUS.FAILED;
196
- });
197
-
198
- const contentData = computed(() => {
199
- return JSON.parse(props.msgInfo.content);
200
- });
201
-
202
- const fileSize = computed(() => {
203
- const size = contentData.value.size;
204
- if (size > 1024 * 1024) {
205
- return Math.round(size / 1024 / 1024) + "M";
206
- }
207
- if (size > 1024) {
208
- return Math.round(size / 1024) + "KB";
209
- }
210
- return size + "B";
211
- });
212
-
213
- const menuItems = computed(() => {
214
- const items = [];
215
- if (props.msgInfo.type === MESSAGE_TYPE.TEXT) {
216
- items.push({
217
- key: 'COPY',
218
- name: '复制',
219
- icon: 'bars'
220
- });
221
- }
222
- if (props.msgInfo.selfSend && props.msgInfo.id > 0) {
223
- items.push({
224
- key: 'RECALL',
225
- name: '撤回',
226
- icon: 'refreshempty'
227
- });
228
- }
229
- items.push({
230
- key: 'DELETE',
231
- name: '删除',
232
- icon: 'trash',
233
- color: '#e64e4e'
234
- });
235
- if (props.msgInfo.type === MESSAGE_TYPE.FILE) {
236
- items.push({
237
- key: 'DOWNLOAD',
238
- name: '下载并打开',
239
- icon: 'download'
240
- });
241
- }
242
- return items;
243
- });
244
-
245
- const isTextMessage = computed(() => {
246
- return props.msgInfo.type === MESSAGE_TYPE.TEXT;
247
- });
248
-
249
- const isAction = computed(() => {
250
- return messageType.isAction(props.msgInfo.type);
251
- });
252
-
253
- const isNormal = computed(() => {
254
- const type = props.msgInfo.type;
255
- return messageType.isNormal(type) || messageType.isAction(type);
256
- });
257
-
258
- const nodesText = computed(() => {
259
- const color = props.msgInfo.selfSend ? 'white' : '';
260
- let text = dom.html2Escape(props.msgInfo.content);
261
- text = url.replaceURLWithHTMLLinks(text, color);
262
- return emoji.transform(text, 'emoji-normal');
263
- });
264
-
265
- const imageStyle = computed(() => {
266
- const systemInfo = uni.getWindowInfo();
267
- const maxSize = systemInfo.windowWidth * 0.6;
268
- const minSize = systemInfo.windowWidth * 0.2;
269
- const width = contentData.value.width;
270
- const height = contentData.value.height;
271
-
272
- if (width && height) {
273
- const ratio = Math.min(width, height) / Math.max(width, height);
274
- const w = Math.max(width > height ? maxSize : ratio * maxSize, minSize);
275
- const h = Math.max(width > height ? ratio * maxSize : maxSize, minSize);
276
- return `width: ${w}px;height:${h}px;`;
277
- } else {
278
- // 兼容历史版本,历史数据没有记录宽高
279
- return `max-width: ${maxSize}px;min-width:100px;max-height: ${maxSize}px;min-height:100px;`;
280
- }
281
- });
282
- </script>
283
-
284
- <style scoped lang="scss">
285
- .message-item {
286
- padding: 2rpx 20rpx;
287
-
288
- .message-tip {
289
- line-height: 60rpx;
290
- text-align: center;
291
- color: $im-text-color-lighter;
292
- font-size: $im-font-size-smaller-extra;
293
- padding: 10rpx;
294
- }
295
-
296
- .message-normal {
297
- position: relative;
298
- margin-bottom: 22rpx;
299
- padding-left: 110rpx;
300
- min-height: 80rpx;
301
-
302
- .avatar {
303
- position: absolute;
304
- top: 0;
305
- left: 0;
306
- }
307
-
308
- .content {
309
- text-align: left;
310
-
311
- .top {
312
- display: flex;
313
- flex-wrap: nowrap;
314
- color: $im-text-color-lighter;
315
- font-size: $im-font-size-smaller;
316
- line-height: $im-font-size-smaller;
317
- height: $im-font-size-smaller;
318
- }
319
-
320
- .bottom {
321
- display: inline-block;
322
- padding-right: 80rpx;
323
- margin-top: 5rpx;
324
-
325
- .message-content-wrapper {
326
- position: relative;
327
- display: flex;
328
- align-items: center;
329
-
330
- .sending {
331
- position: relative;
332
- margin: 0 6rpx;
333
-
334
- .icon-loading {
335
- color: $im-color-primary;
336
- }
337
- }
338
-
339
- .send-fail {
340
- color: #e60c0c;
341
- font-size: 50rpx;
342
- margin: 0 5rpx;
343
- }
344
-
345
- .message-text {
346
- position: relative;
347
- line-height: 1.6;
348
- margin-top: 10rpx;
349
- padding: 16rpx 24rpx;
350
- background-color: $im-bg;
351
- border-radius: 20rpx;
352
- color: $im-text-color;
353
- font-size: $im-font-size;
354
- text-align: left;
355
- display: block;
356
- word-break: break-word;
357
- white-space: pre-line;
358
-
359
- &:after {
360
- content: "";
361
- position: absolute;
362
- left: -20rpx;
363
- top: 26rpx;
364
- width: 6rpx;
365
- height: 6rpx;
366
- border-style: solid dashed dashed;
367
- border-color: $im-bg transparent transparent;
368
- overflow: hidden;
369
- border-width: 18rpx;
370
- }
371
- }
372
-
373
- .message-image {
374
- display: flex;
375
- flex-wrap: nowrap;
376
- flex-direction: row;
377
- align-items: center;
378
-
379
- .image-box {
380
- position: relative;
381
-
382
- .send-image {
383
- cursor: pointer;
384
- border-radius: 10rpx;
385
- background: $im-bg;
386
- border: 6rpx solid $im-color-primary-light-5;
387
- }
388
- }
389
-
390
- .send-fail {
391
- color: $im-color-danger;
392
- font-size: $im-font-size;
393
- cursor: pointer;
394
- margin: 0 20px;
395
- }
396
- }
397
-
398
- .message-file {
399
- display: flex;
400
- flex-wrap: nowrap;
401
- flex-direction: row;
402
- align-items: center;
403
- cursor: pointer;
404
-
405
- .file-box {
406
- position: relative;
407
- display: flex;
408
- flex-wrap: nowrap;
409
- align-items: center;
410
- min-height: 60px;
411
- border-radius: 4px;
412
- padding: 10px 15px;
413
- box-shadow: $im-box-shadow-dark;
414
-
415
- .file-info {
416
- flex: 1;
417
- height: 100%;
418
- text-align: left;
419
- font-size: 14px;
420
- width: 300rpx;
421
-
422
- .file-name {
423
- font-weight: 600;
424
- margin-bottom: 15px;
425
- word-break: break-all;
426
- }
427
- }
428
-
429
- .file-icon {
430
- font-size: 80rpx;
431
- color: #d42e07;
432
- }
433
- }
434
-
435
- .send-fail {
436
- color: #e60c0c;
437
- font-size: 50rpx;
438
- cursor: pointer;
439
- margin: 0 20rpx;
440
- }
441
- }
442
-
443
- .message-audio {
444
- display: flex;
445
- align-items: center;
446
-
447
- .chat-audio-text {
448
- padding-right: 8px;
449
- }
450
-
451
- .icon-voice-play {
452
- font-size: 18px;
453
- padding-right: 8px;
454
- }
455
- }
456
- }
457
-
458
- .chat-realtime {
459
- display: flex;
460
- align-items: center;
461
-
462
- .iconfont {
463
- font-size: 20px;
464
- padding-right: 8px;
465
- }
466
- }
467
-
468
- .message-status {
469
- line-height: $im-font-size-smaller-extra;
470
- font-size: $im-font-size-smaller-extra;
471
- padding-top: 2rpx;
472
-
473
- .chat-readed {
474
- display: block;
475
- padding-top: 2rpx;
476
- color: $im-text-color-lighter;
477
- }
478
-
479
- .chat-unread {
480
- color: $im-color-danger;
481
- }
482
- }
483
-
484
- .chat-receipt {
485
- font-size: $im-font-size-smaller;
486
- color: $im-text-color-lighter;
487
- font-weight: 600;
488
-
489
- .icon-ok {
490
- font-size: 20px;
491
- color: $im-color-success;
492
- }
493
- }
494
- }
495
- }
496
-
497
- &.message-mine {
498
- text-align: right;
499
- padding-left: 0;
500
- padding-right: 110rpx;
501
-
502
- .avatar {
503
- left: auto;
504
- right: 0;
505
- }
506
-
507
- .content {
508
- text-align: right;
509
-
510
- .bottom {
511
- padding-left: 80rpx;
512
- padding-right: 0;
513
-
514
- .message-content-wrapper {
515
- flex-direction: row-reverse;
516
- }
517
-
518
- .message-text {
519
- margin-left: 10px;
520
- background-color: $im-color-primary-light-2;
521
- color: #fff;
522
-
523
- &:after {
524
- left: auto;
525
- right: -9px;
526
- border-top-color: $im-color-primary-light-2;
527
- }
528
- }
529
-
530
- .message-audio {
531
- flex-direction: row-reverse;
532
-
533
- .chat-audio-text {
534
- padding-right: 0;
535
- padding-left: 8px;
536
- }
537
-
538
- .icon-voice-play {
539
- transform: rotateY(180deg);
540
- }
541
- }
542
-
543
- .chat-realtime {
544
- display: flex;
545
- flex-direction: row-reverse;
546
-
547
- .iconfont {
548
- transform: rotateY(180deg);
549
- }
550
- }
551
- }
552
- }
553
- }
554
- }
555
- }
1
+ <template>
2
+ <view class="message-item">
3
+ <view class="message-tip" v-if="msgInfo.type == MESSAGE_TYPE.TIP_TEXT">
4
+ {{ msgInfo.content }}
5
+ </view>
6
+ <view class="message-tip" v-else-if="msgInfo.type == MESSAGE_TYPE.TIP_TIME">
7
+ {{ $datetime.toTimeText(msgInfo.sendTime) }}
8
+ </view>
9
+ <view class="message-normal" v-else-if="isNormal" :class="{ 'message-mine': msgInfo.selfSend }">
10
+ <im-avatar class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="avatar"
11
+ :name="showName" size="small" />
12
+ <view class="content">
13
+ <view v-if="msgInfo.groupId && !msgInfo.selfSend" class="top">
14
+ <text>{{ showName }}</text>
15
+ </view>
16
+ <view class="bottom">
17
+ <view class="message-content-wrapper">
18
+ <view v-if="msgInfo.type == MESSAGE_TYPE.TEXT">
19
+ <im-context-menu :items="menuItems" @select="onSelectMenu">
20
+ <!-- up-parse支持点击a标签,但是不支持显示emo表情,也不支持换行 -->
21
+ <up-parse
22
+ v-if="$url.containUrl(msgInfo.content) && !emoji.containEmoji(msgInfo.content)"
23
+ class="message-text" :showImgMenu="false" :content="nodesText" />
24
+ <!-- rich-text支持显示emo表情以及消息换行,但是不支持点击a标签 -->
25
+ <rich-text v-else class="message-text" :nodes="nodesText" />
26
+ </im-context-menu>
27
+ </view>
28
+ <view class="message-image" v-else-if="msgInfo.type == MESSAGE_TYPE.IMAGE">
29
+ <im-context-menu :items="menuItems" @select="onSelectMenu">
30
+ <view class="image-box">
31
+ <image class="send-image" :style="imageStyle" mode="aspectFill"
32
+ :src="contentData.thumbUrl" lazy-load="true" @click.stop="onShowFullImage()">
33
+ </image>
34
+ <im-loading v-if="sending"></im-loading>
35
+ </view>
36
+ </im-context-menu>
37
+ </view>
38
+ <view class="message-file" v-else-if="msgInfo.type == MESSAGE_TYPE.FILE">
39
+ <im-context-menu :items="menuItems" @select="onSelectMenu">
40
+ <view class="file-box">
41
+ <view class="file-info" style="overflow: hidden;">
42
+ <u-link class="file-name" :under-line="true" color="#007BFF"
43
+ :href="contentData.url" :text="contentData.name"></u-link>
44
+ <view class="file-size">{{ fileSize }}</view>
45
+ </view>
46
+ <view class="file-icon iconfont icon-file"></view>
47
+ <im-loading v-if="sending"></im-loading>
48
+ </view>
49
+ </im-context-menu>
50
+ </view>
51
+ <im-context-menu v-else-if="msgInfo.type == MESSAGE_TYPE.AUDIO" :items="menuItems"
52
+ @select="onSelectMenu">
53
+ <view class="message-audio message-text" @click="onPlayAudio()">
54
+ <text class="iconfont icon-voice-play"></text>
55
+ <text class="chat-audio-text">{{ contentData.duration + '"' }}</text>
56
+ <text v-if="audioPlayState == 'PAUSE'" class="iconfont icon-play"></text>
57
+ <text v-if="audioPlayState == 'PLAYING'" class="iconfont icon-pause"></text>
58
+ </view>
59
+ </im-context-menu>
60
+ <im-context-menu v-if="isAction" :items="menuItems" @select="onSelectMenu">
61
+ <view class="chat-realtime message-text" @click="$emit('call')">
62
+ <text v-if="msgInfo.type == MESSAGE_TYPE.ACT_RT_VOICE"
63
+ class="iconfont icon-chat-voice"></text>
64
+ <text v-if="msgInfo.type == MESSAGE_TYPE.ACT_RT_VIDEO"
65
+ class="iconfont icon-chat-video"></text>
66
+ <text>{{ msgInfo.content }}</text>
67
+ </view>
68
+ </im-context-menu>
69
+ <view v-if="sending && isTextMessage" class="sending">
70
+ <im-loading :size="40" icon-color="#656adf" :mask="false"></im-loading>
71
+ </view>
72
+ <view v-else-if="sendFail" @click="onSendFail"
73
+ class="send-fail iconfont icon-warning-circle-fill"></view>
74
+ </view>
75
+ <view class="message-status" v-if="!isAction && msgInfo.selfSend && !msgInfo.groupId">
76
+ <text class="chat-readed" v-if="msgInfo.status == MESSAGE_STATUS.READED">已读</text>
77
+ <text class="chat-unread" v-else>未读</text>
78
+ </view>
79
+ <view class="chat-receipt" v-if="msgInfo.receipt" @click="onShowReadedBox">
80
+ <text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text>
81
+ <text v-else>{{ msgInfo.readedCount }}人已读</text>
82
+ </view>
83
+ </view>
84
+ </view>
85
+ </view>
86
+ <im-read-receipt ref="chatGroupReaded" :groupMembers="groupMembers as any[]" :msgInfo="msgInfo" />
87
+ </view>
88
+ </template>
89
+
90
+ <script setup lang="ts">
91
+ import { ref, computed } from 'vue'
92
+ import ImAvatar from '../im-avatar/im-avatar.vue'
93
+ import ImReadReceipt from '../im-read-receipt/im-read-receipt.vue'
94
+ import ImContextMenu from '../im-context-menu/im-context-menu.vue'
95
+ import { MESSAGE_TYPE, MESSAGE_STATUS, datetime, dom, url, messageType, emoji } from '../../index'
96
+
97
+ interface Props {
98
+ avatar?: string;
99
+ showName: string;
100
+ msgInfo: any;
101
+ groupMembers?: any[];
102
+ }
103
+
104
+ interface Emits {
105
+ (e: 'call'): void;
106
+ (e: 'longPressHead'): void;
107
+ (e: 'resend', msgInfo: any): void;
108
+ (e: 'audioStateChange', state: string, msgInfo: any): void;
109
+ (e: 'copy', msgInfo: any): void;
110
+ (e: 'recall', msgInfo: any): void;
111
+ (e: 'delete', msgInfo: any): void;
112
+ (e: 'download', msgInfo: any): void;
113
+ }
114
+
115
+ const $url = url
116
+ const $datetime = datetime
117
+ const props = defineProps<Props>();
118
+ const emit = defineEmits<Emits>();
119
+
120
+ const audioPlayState = ref('STOP');
121
+ const innerAudioContext = ref<UniApp.InnerAudioContext | null>(null);
122
+ const menu = ref({
123
+ show: false,
124
+ style: ""
125
+ });
126
+
127
+ const onSendFail = () => {
128
+ emit("resend", props.msgInfo);
129
+ };
130
+
131
+ const onPlayAudio = () => {
132
+ // 初始化音频播放器
133
+ if (!innerAudioContext.value) {
134
+ innerAudioContext.value = uni.createInnerAudioContext();
135
+ const url = contentData.value.url;
136
+ innerAudioContext.value.src = url;
137
+ innerAudioContext.value.onEnded(() => {
138
+ console.log('停止');
139
+ audioPlayState.value = "STOP";
140
+ emitAudioState();
141
+ });
142
+ innerAudioContext.value.onError(() => {
143
+ audioPlayState.value = "STOP";
144
+ console.log("播放音频出错");
145
+ emitAudioState();
146
+ });
147
+ }
148
+
149
+ if (audioPlayState.value === 'STOP') {
150
+ innerAudioContext.value.play();
151
+ audioPlayState.value = "PLAYING";
152
+ } else if (audioPlayState.value === 'PLAYING') {
153
+ innerAudioContext.value.pause();
154
+ audioPlayState.value = "PAUSE";
155
+ } else if (audioPlayState.value === 'PAUSE') {
156
+ innerAudioContext.value.play();
157
+ audioPlayState.value = "PLAYING";
158
+ }
159
+ emitAudioState();
160
+ };
161
+
162
+ const onSelectMenu = (item: any) => {
163
+ emit(item.key.toLowerCase() as any, props.msgInfo);
164
+ menu.value.show = false;
165
+ };
166
+
167
+ const onShowFullImage = () => {
168
+ const imageUrl = contentData.value.originUrl;
169
+ uni.previewImage({
170
+ urls: [imageUrl]
171
+ });
172
+ };
173
+
174
+ const onShowReadedBox = () => {
175
+ // this.$refs.chatGroupReaded.open();
176
+ };
177
+
178
+ const emitAudioState = () => {
179
+ emit("audioStateChange", audioPlayState.value, props.msgInfo);
180
+ };
181
+
182
+ const stopPlayAudio = () => {
183
+ if (innerAudioContext.value) {
184
+ innerAudioContext.value.stop();
185
+ innerAudioContext.value = null;
186
+ audioPlayState.value = "STOP";
187
+ }
188
+ };
189
+
190
+ const sending = computed(() => {
191
+ return props.msgInfo.status === MESSAGE_STATUS.SENDING;
192
+ });
193
+
194
+ const sendFail = computed(() => {
195
+ return props.msgInfo.status === MESSAGE_STATUS.FAILED;
196
+ });
197
+
198
+ const contentData = computed(() => {
199
+ return JSON.parse(props.msgInfo.content);
200
+ });
201
+
202
+ const fileSize = computed(() => {
203
+ const size = contentData.value.size;
204
+ if (size > 1024 * 1024) {
205
+ return Math.round(size / 1024 / 1024) + "M";
206
+ }
207
+ if (size > 1024) {
208
+ return Math.round(size / 1024) + "KB";
209
+ }
210
+ return size + "B";
211
+ });
212
+
213
+ const menuItems = computed(() => {
214
+ const items = [];
215
+ if (props.msgInfo.type === MESSAGE_TYPE.TEXT) {
216
+ items.push({
217
+ key: 'COPY',
218
+ name: '复制',
219
+ icon: 'bars'
220
+ });
221
+ }
222
+ if (props.msgInfo.selfSend && props.msgInfo.id > 0) {
223
+ items.push({
224
+ key: 'RECALL',
225
+ name: '撤回',
226
+ icon: 'refreshempty'
227
+ });
228
+ }
229
+ items.push({
230
+ key: 'DELETE',
231
+ name: '删除',
232
+ icon: 'trash',
233
+ color: '#e64e4e'
234
+ });
235
+ if (props.msgInfo.type === MESSAGE_TYPE.FILE) {
236
+ items.push({
237
+ key: 'DOWNLOAD',
238
+ name: '下载并打开',
239
+ icon: 'download'
240
+ });
241
+ }
242
+ return items;
243
+ });
244
+
245
+ const isTextMessage = computed(() => {
246
+ return props.msgInfo.type === MESSAGE_TYPE.TEXT;
247
+ });
248
+
249
+ const isAction = computed(() => {
250
+ return messageType.isAction(props.msgInfo.type);
251
+ });
252
+
253
+ const isNormal = computed(() => {
254
+ const type = props.msgInfo.type;
255
+ return messageType.isNormal(type) || messageType.isAction(type);
256
+ });
257
+
258
+ const nodesText = computed(() => {
259
+ const color = props.msgInfo.selfSend ? 'white' : '';
260
+ let text = dom.html2Escape(props.msgInfo.content);
261
+ text = url.replaceURLWithHTMLLinks(text, color);
262
+ return emoji.transform(text, 'emoji-normal');
263
+ });
264
+
265
+ const imageStyle = computed(() => {
266
+ const systemInfo = uni.getWindowInfo();
267
+ const maxSize = systemInfo.windowWidth * 0.6;
268
+ const minSize = systemInfo.windowWidth * 0.2;
269
+ const width = contentData.value.width;
270
+ const height = contentData.value.height;
271
+
272
+ if (width && height) {
273
+ const ratio = Math.min(width, height) / Math.max(width, height);
274
+ const w = Math.max(width > height ? maxSize : ratio * maxSize, minSize);
275
+ const h = Math.max(width > height ? ratio * maxSize : maxSize, minSize);
276
+ return `width: ${w}px;height:${h}px;`;
277
+ } else {
278
+ // 兼容历史版本,历史数据没有记录宽高
279
+ return `max-width: ${maxSize}px;min-width:100px;max-height: ${maxSize}px;min-height:100px;`;
280
+ }
281
+ });
282
+ </script>
283
+
284
+ <style scoped lang="scss">
285
+ .message-item {
286
+ padding: 2rpx 20rpx;
287
+
288
+ .message-tip {
289
+ line-height: 60rpx;
290
+ text-align: center;
291
+ color: $im-text-color-lighter;
292
+ font-size: $im-font-size-mini;
293
+ padding: 10rpx;
294
+ }
295
+
296
+ .message-normal {
297
+ position: relative;
298
+ margin-bottom: 22rpx;
299
+ padding-left: 110rpx;
300
+ min-height: 80rpx;
301
+
302
+ .avatar {
303
+ position: absolute;
304
+ top: 0;
305
+ left: 0;
306
+ }
307
+
308
+ .content {
309
+ text-align: left;
310
+
311
+ .top {
312
+ display: flex;
313
+ flex-wrap: nowrap;
314
+ color: $im-text-color-lighter;
315
+ font-size: $im-font-size-small;
316
+ line-height: $im-font-size-small;
317
+ height: $im-font-size-small;
318
+ }
319
+
320
+ .bottom {
321
+ display: inline-block;
322
+ padding-right: 80rpx;
323
+ margin-top: 5rpx;
324
+
325
+ .message-content-wrapper {
326
+ position: relative;
327
+ display: flex;
328
+ align-items: center;
329
+
330
+ .sending {
331
+ position: relative;
332
+ margin: 0 6rpx;
333
+
334
+ .icon-loading {
335
+ color: $im-color-primary;
336
+ }
337
+ }
338
+
339
+ .send-fail {
340
+ color: #e60c0c;
341
+ font-size: 50rpx;
342
+ margin: 0 5rpx;
343
+ }
344
+
345
+ .message-text {
346
+ position: relative;
347
+ line-height: 1.6;
348
+ margin-top: 10rpx;
349
+ padding: 16rpx 24rpx;
350
+ background-color: $im-bg;
351
+ border-radius: 20rpx;
352
+ color: $im-text-color;
353
+ font-size: $im-font-size;
354
+ text-align: left;
355
+ display: block;
356
+ word-break: break-word;
357
+ white-space: pre-line;
358
+
359
+ &:after {
360
+ content: "";
361
+ position: absolute;
362
+ left: -20rpx;
363
+ top: 26rpx;
364
+ width: 6rpx;
365
+ height: 6rpx;
366
+ border-style: solid dashed dashed;
367
+ border-color: $im-bg transparent transparent;
368
+ overflow: hidden;
369
+ border-width: 18rpx;
370
+ }
371
+ }
372
+
373
+ .message-image {
374
+ display: flex;
375
+ flex-wrap: nowrap;
376
+ flex-direction: row;
377
+ align-items: center;
378
+
379
+ .image-box {
380
+ position: relative;
381
+
382
+ .send-image {
383
+ cursor: pointer;
384
+ border-radius: 10rpx;
385
+ background: $im-bg;
386
+ border: 6rpx solid $im-color-primary-light-5;
387
+ }
388
+ }
389
+
390
+ .send-fail {
391
+ color: $im-color-danger;
392
+ font-size: $im-font-size;
393
+ cursor: pointer;
394
+ margin: 0 20px;
395
+ }
396
+ }
397
+
398
+ .message-file {
399
+ display: flex;
400
+ flex-wrap: nowrap;
401
+ flex-direction: row;
402
+ align-items: center;
403
+ cursor: pointer;
404
+
405
+ .file-box {
406
+ position: relative;
407
+ display: flex;
408
+ flex-wrap: nowrap;
409
+ align-items: center;
410
+ min-height: 60px;
411
+ border-radius: 4px;
412
+ padding: 10px 15px;
413
+ box-shadow: $im-box-shadow-dark;
414
+
415
+ .file-info {
416
+ flex: 1;
417
+ height: 100%;
418
+ text-align: left;
419
+ font-size: 14px;
420
+ width: 300rpx;
421
+
422
+ .file-name {
423
+ font-weight: 600;
424
+ margin-bottom: 15px;
425
+ word-break: break-all;
426
+ }
427
+ }
428
+
429
+ .file-icon {
430
+ font-size: 80rpx;
431
+ color: #d42e07;
432
+ }
433
+ }
434
+
435
+ .send-fail {
436
+ color: #e60c0c;
437
+ font-size: 50rpx;
438
+ cursor: pointer;
439
+ margin: 0 20rpx;
440
+ }
441
+ }
442
+
443
+ .message-audio {
444
+ display: flex;
445
+ align-items: center;
446
+
447
+ .chat-audio-text {
448
+ padding-right: 8px;
449
+ }
450
+
451
+ .icon-voice-play {
452
+ font-size: 18px;
453
+ padding-right: 8px;
454
+ }
455
+ }
456
+ }
457
+
458
+ .chat-realtime {
459
+ display: flex;
460
+ align-items: center;
461
+
462
+ .iconfont {
463
+ font-size: 20px;
464
+ padding-right: 8px;
465
+ }
466
+ }
467
+
468
+ .message-status {
469
+ line-height: $im-font-size-mini;
470
+ font-size: $im-font-size-mini;
471
+ padding-top: 2rpx;
472
+
473
+ .chat-readed {
474
+ display: block;
475
+ padding-top: 2rpx;
476
+ color: $im-text-color-lighter;
477
+ }
478
+
479
+ .chat-unread {
480
+ color: $im-color-danger;
481
+ }
482
+ }
483
+
484
+ .chat-receipt {
485
+ font-size: $im-font-size-small;
486
+ color: $im-text-color-lighter;
487
+ font-weight: 600;
488
+
489
+ .icon-ok {
490
+ font-size: 20px;
491
+ color: $im-color-success;
492
+ }
493
+ }
494
+ }
495
+ }
496
+
497
+ &.message-mine {
498
+ text-align: right;
499
+ padding-left: 0;
500
+ padding-right: 110rpx;
501
+
502
+ .avatar {
503
+ left: auto;
504
+ right: 0;
505
+ }
506
+
507
+ .content {
508
+ text-align: right;
509
+
510
+ .bottom {
511
+ padding-left: 80rpx;
512
+ padding-right: 0;
513
+
514
+ .message-content-wrapper {
515
+ flex-direction: row-reverse;
516
+ }
517
+
518
+ .message-text {
519
+ margin-left: 10px;
520
+ background-color: $im-color-primary-light-2;
521
+ color: #fff;
522
+
523
+ &:after {
524
+ left: auto;
525
+ right: -9px;
526
+ border-top-color: $im-color-primary-light-2;
527
+ }
528
+ }
529
+
530
+ .message-audio {
531
+ flex-direction: row-reverse;
532
+
533
+ .chat-audio-text {
534
+ padding-right: 0;
535
+ padding-left: 8px;
536
+ }
537
+
538
+ .icon-voice-play {
539
+ transform: rotateY(180deg);
540
+ }
541
+ }
542
+
543
+ .chat-realtime {
544
+ display: flex;
545
+ flex-direction: row-reverse;
546
+
547
+ .iconfont {
548
+ transform: rotateY(180deg);
549
+ }
550
+ }
551
+ }
552
+ }
553
+ }
554
+ }
555
+ }
556
556
  </style>