@tencentcloud/ai-desk-customer-vue 1.5.11 → 1.6.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 (31) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/assets/audio-blue.svg +4 -0
  3. package/assets/audio_icon_1.svg +3 -0
  4. package/assets/audio_icon_2.svg +3 -0
  5. package/assets/audio_icon_3.svg +3 -0
  6. package/assets/keyboard-icon.svg +9 -0
  7. package/components/CustomerServiceChat/index-web.vue +10 -2
  8. package/components/CustomerServiceChat/message-input/index-web.vue +57 -10
  9. package/components/CustomerServiceChat/message-input/message-input-editor-web.vue +342 -11
  10. package/components/CustomerServiceChat/message-list/index-web.vue +13 -0
  11. package/components/CustomerServiceChat/message-list/message-elements/message-audio-web.vue +50 -78
  12. package/components/CustomerServiceChat/message-list/message-elements/message-bubble-web.vue +1 -1
  13. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/marked.ts +3 -2
  14. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-robot-welcome.vue +13 -6
  15. package/components/CustomerServiceChat/message-list/message-elements/message-text.vue +3 -2
  16. package/components/common/Toast/index-web.ts +16 -3
  17. package/components/common/Toast/index-web.vue +16 -6
  18. package/constant.ts +1 -0
  19. package/locales/en/TUIChat.ts +6 -2
  20. package/locales/fil/TUIChat.ts +6 -2
  21. package/locales/id/TUIChat.ts +76 -72
  22. package/locales/ja/TUIChat.ts +76 -72
  23. package/locales/ms/TUIChat.ts +76 -72
  24. package/locales/ru/TUIChat.ts +6 -2
  25. package/locales/th/TUIChat.ts +76 -72
  26. package/locales/vi/TUIChat.ts +76 -72
  27. package/locales/zh_cn/TUIChat.ts +6 -2
  28. package/locales/zh_tw/TUIChat.ts +6 -2
  29. package/package.json +2 -1
  30. package/server.ts +22 -3
  31. package/assets/keyboard_icon.png +0 -0
@@ -2,10 +2,12 @@
2
2
  <div
3
3
  :class="[
4
4
  'message-input-editor-container',
5
- isPC && !props.hasInputTool && 'message-input-editor-container-no-inputTool',
5
+ isPC && !props.shouldShowToolbar && 'message-input-editor-container-no-inputTool',
6
6
  isH5 && 'message-input-editor-container-h5',
7
- isH5 && !props.hasEmojiTool && props.hasInputTool && 'message-input-editor-container-h5-no-emoji-no-tool',
8
- isH5 && !props.hasEmojiTool && !props.hasInputTool && 'message-input-editor-container-h5-no-emoji',
7
+ isH5 && !props.shouldShowEmoji && props.shouldShowToolbar && 'message-input-editor-container-h5-no-emoji-no-tool',
8
+ isH5 && !props.shouldShowEmoji && !props.shouldShowToolbar && 'message-input-editor-container-h5-no-emoji',
9
+ isH5 && props.shouldShowAudio && 'message-input-editor-container-h5-audio',
10
+ isH5 && isInAudioMode && 'message-input-editor-container-h5-audio-mode',
9
11
  ]"
10
12
  >
11
13
  <div
@@ -14,8 +16,35 @@
14
16
  >
15
17
  {{ muteText }}
16
18
  </div>
19
+ <div v-if="isInAudioMode"
20
+ class="audio-mode no-copy"
21
+ @touchstart="startRecording"
22
+ @touchend="stopRecording"
23
+ @touchcancel="stopRecording"
24
+ @touchmove="handleTouchMove"
25
+ @contextmenu.prevent
26
+ >
27
+ {{ TUITranslateService.t('TUIChat.按住说话') }}
28
+ <div v-if="isRecording" class="record-container">
29
+ <div class="record-tip">
30
+ {{ recordTip }}
31
+ </div>
32
+ <div class="audio-wave-container">
33
+ <div :class="['audio-wave-bubble', recordCancel ? 'audio-wave-bubble-cancel' : '']">
34
+ <div class="audio-wave">
35
+ <div
36
+ v-for="(bar, index) in audioWaveBars"
37
+ :key="index"
38
+ class="wave-bar"
39
+ :style="{ height: bar.height + 'px' }"
40
+ ></div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
17
46
  <div
18
- v-if="!isMuted && enableInput"
47
+ v-if="!isMuted && enableInput && !isInAudioMode"
19
48
  ref="editorDom"
20
49
  class="message-input-editor-area"
21
50
  :contenteditable="isH5"
@@ -30,10 +59,12 @@
30
59
  </template>
31
60
  <script setup lang="ts">
32
61
  import vue from '../../../adapter-vue';
33
- import {
62
+ import TUIChatEngine, {
34
63
  TUIStore,
35
64
  StoreName,
36
65
  IMessageModel,
66
+ TUIChatService,
67
+ TUITranslateService,
37
68
  } from '@tencentcloud/chat-uikit-engine';
38
69
  import { Editor, JSONContent } from '@tiptap/core';
39
70
  import Document from '@tiptap/extension-document';
@@ -46,6 +77,14 @@ import { ITipTapEditorContent } from '../../../interface';
46
77
  import { parseTextToRenderArray } from '../emoji-config';
47
78
  import { isH5, isPC } from '../../../utils/env';
48
79
  import DraftManager from '../utils/conversationDraft';
80
+ import {
81
+ Toast,
82
+ TOAST_TYPE,
83
+ } from '../../common/Toast/index-web';
84
+ import Recorder from "js-audio-recorder";
85
+ import { isEnabledMessageReadReceiptGlobal } from '../../../utils/utils';
86
+ import Log from '../../../utils/logger';
87
+ import state from '../../../utils/state.js';
49
88
  const { toRefs, ref, onMounted, watch, onUnmounted } = vue;
50
89
 
51
90
  const props = defineProps({
@@ -77,14 +116,22 @@ const props = defineProps({
77
116
  type: Boolean,
78
117
  default: true,
79
118
  },
80
- hasEmojiTool: {
119
+ isInAudioMode:{
120
+ type: Boolean,
121
+ default: false,
122
+ },
123
+ shouldShowEmoji: {
81
124
  type: Boolean,
82
125
  default: true,
83
126
  },
84
- hasInputTool: {
127
+ shouldShowToolbar: {
85
128
  type: Boolean,
86
129
  default: true,
87
- }
130
+ },
131
+ shouldShowAudio: {
132
+ type: Boolean,
133
+ default: false,
134
+ },
88
135
  });
89
136
 
90
137
  const emits = defineEmits(['sendMessage', 'onTyping', 'blurToolAndEmojiH5', 'isInputNotEmpty']);
@@ -97,6 +144,17 @@ const currentQuoteMessage = ref<{ message: IMessageModel; type: string }>();
97
144
  const editorDom = ref();
98
145
  let editor: Editor | null = null;
99
146
  const fileMap = new Map<string, any>();
147
+ const recorder = ref();
148
+ const recordTime = ref(0);
149
+ const isInAudioMode = ref(props.isInAudioMode);
150
+ const isRecording = ref(false);
151
+ let startRecordY = 0;
152
+ const recordCancel = ref(false);
153
+ const recordTip = ref(TUITranslateService.t('TUIChat.松开发送'));
154
+ let recordTimer: any = null;
155
+ const audioWaveBars = ref(Array(24).fill({ height: 6 }));
156
+ let audioWaveAnimation = -1;
157
+ let pressStartTime: any = null;
100
158
 
101
159
  function onCurrentConversationIDUpdated(conversationID: string) {
102
160
  if (currentConversationID.value !== conversationID) {
@@ -134,6 +192,13 @@ function focusEditor() {
134
192
  }
135
193
  }
136
194
 
195
+ watch(
196
+ () => [props.isInAudioMode],
197
+ (newValue) => {
198
+ isInAudioMode.value = newValue[0];
199
+ }
200
+ );
201
+
137
202
  onMounted(() => {
138
203
  editor = isPC
139
204
  ? new Editor({
@@ -209,6 +274,15 @@ onMounted(() => {
209
274
  TUIStore.watch(StoreName.CHAT, {
210
275
  quoteMessage: onQuoteMessageUpdated,
211
276
  });
277
+
278
+ document.addEventListener('visibilitychange', handleVisibilityChange);
279
+
280
+ recorder.value = new Recorder({
281
+ sampleBits: 16,
282
+ sampleRate: 44100,
283
+ numChannels: 1,
284
+ compiling: false,
285
+ });
212
286
  });
213
287
 
214
288
  onUnmounted(() => {
@@ -222,6 +296,8 @@ onUnmounted(() => {
222
296
 
223
297
  // clear map store
224
298
  fileMap.clear();
299
+ initRecorder();
300
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
225
301
  });
226
302
 
227
303
  function handleEnter(e: any) {
@@ -250,9 +326,12 @@ function handleH5Blur() {
250
326
  isH5 && (isEditorBlur.value = true);
251
327
  }
252
328
 
253
- async function handleH5Focus() {
329
+ async function handleH5Focus(e: any) {
254
330
  emits('blurToolAndEmojiH5');
255
331
  isH5 && (isEditorBlur.value = false);
332
+ if (e.target) {
333
+ e.target.scrollIntoView({ behavior: 'smooth', block: 'center' });
334
+ }
256
335
  }
257
336
 
258
337
  function handlePCFileDrop(e: any) {
@@ -619,7 +698,9 @@ function resetEditor() {
619
698
  editor?.commands?.clearContent(true);
620
699
  isEditorBlur.value = true;
621
700
  isEditorEmpty.value = true;
622
- isH5 && (editorDom.value.innerHTML = '');
701
+ if (!isInAudioMode.value) {
702
+ isH5 && (editorDom.value.innerHTML = '');
703
+ }
623
704
  }
624
705
 
625
706
  function getEditorHTML(): string {
@@ -712,7 +793,163 @@ watch(
712
793
  deep: true,
713
794
  },
714
795
  );
796
+ async function initRecorder() {
797
+ clearInterval(recordTimer);
798
+ recordTimer = undefined;
799
+ recordTime.value = 0;
800
+ await recorder.value.stop();
801
+ cancelAnimationFrame(audioWaveAnimation);
802
+ }
803
+
804
+ watch(
805
+ recordTime,
806
+ (newVal: number) => {
807
+ if(newVal >= 50 && newVal < 60) {
808
+ let tip = '';
809
+ const currentLanguage = state.get('currentLanguage');
810
+ if (currentLanguage === 'zh' || currentLanguage === 'zh_tw' || currentLanguage === 'ja') {
811
+ tip = `${10 - newVal}${TUITranslateService.t('TUIChat.录音结束提醒')}`
812
+ } else {
813
+ tip = `${TUITranslateService.t('TUIChat.录音结束提醒')} ${10 - newVal}s`
814
+ }
815
+ recordTip.value = tip;
816
+ } else if (newVal >= 60) {
817
+ stopRecording();
818
+ }
819
+ }
820
+ );
821
+
822
+ const startRecording = async(e) => {
823
+ if (isRecording.value) {
824
+ return;
825
+ }
826
+ isRecording.value = true;
827
+ pressStartTime = new Date();
828
+ recordCancel.value = false;
829
+ Recorder.getPermission().then(async() => {
830
+ if (!isRecording.value) {
831
+ return;
832
+ }
833
+ if (recordTimer !== undefined) {
834
+ initRecorder();
835
+ }
836
+ recorder.value.start().then(() => {
837
+ updateRecordWave();
838
+ startRecordY = getRecordY(e);
839
+ recordTimer = setInterval(() => {
840
+ recordTime.value = recordTime.value + 1;
841
+ }, 1000);
842
+ }, (error) => {
843
+ Log.e(`Error in recording ${error.name} : ${error.message}`);
844
+ isRecording.value = false;
845
+ let toastMessage = `${error.name} : ${error.message}`;
846
+ Toast({
847
+ message:toastMessage,
848
+ type: TOAST_TYPE.ERROR,
849
+ duration: 2000,
850
+ });
851
+ initRecorder();
852
+ });
853
+ }, (error) => {
854
+ isRecording.value = false;
855
+ initRecorder();
856
+ Toast({
857
+ message:TUITranslateService.t("TUIChat.请检查麦克风访问权限"),
858
+ type: TOAST_TYPE.ERROR,
859
+ duration: 2000,
860
+ });
861
+ Log.w(`Error in mic permission ${error.name} : ${error.message}`);
862
+ });
863
+ e.preventDefault();
864
+ };
715
865
 
866
+ const stopRecording = async () => {
867
+ isRecording.value = false;
868
+ const duration = recordTime.value;
869
+ initRecorder();
870
+ const pressDuration = Date.now() - pressStartTime;
871
+ if (pressDuration < 1000) {
872
+ // 按压时间过短,还没开始录音就结束
873
+ Toast({
874
+ message: TUITranslateService.t("TUIChat.按压时间过短,请按压超过1秒"),
875
+ type: TOAST_TYPE.ERROR,
876
+ duration: 3000,
877
+ });
878
+ } else if (!recordCancel.value && duration) {
879
+ // 已经录音 && 没有取消录音
880
+ if (duration <= 60) {
881
+ let wavBlob = recorder.value.getWAVBlob();
882
+ let tempFilePath = new File([wavBlob], `ai_desk_customer_vue_${Date.now()}.wav`, { type: "wav" });
883
+ (tempFilePath as any).duration = duration * 1000;
884
+ if (tempFilePath && duration <= 60) {
885
+ try {
886
+ TUIChatService.sendAudioMessage({
887
+ to: currentConversationID.value.replace(TUIChatEngine.TYPES.CONV_C2C, ''),
888
+ conversationType: TUIChatEngine.TYPES.CONV_C2C,
889
+ payload: { file: tempFilePath },
890
+ needReadReceipt: isEnabledMessageReadReceiptGlobal(),
891
+ });
892
+ } catch (error) {
893
+ const message = `${TUITranslateService.t("TUIChat.发送失败")}: ${error}`;
894
+ Toast({
895
+ message,
896
+ type: TOAST_TYPE.ERROR,
897
+ duration: 2000,
898
+ });
899
+ }
900
+ }
901
+ }
902
+ }
903
+ };
904
+
905
+ const getRecordY = (e) => {
906
+ return e.type.includes('touch') ? e.touches[0].clientY : e.clientY;
907
+ }
908
+
909
+ const handleTouchMove = (e) => {
910
+ if (!isRecording.value) return;
911
+ let currentRecordY = getRecordY(e);
912
+ let deltaY = Math.abs(currentRecordY - startRecordY);
913
+ if (recordTime.value <= 50) {
914
+ if (deltaY > 40) {
915
+ recordCancel.value = true;
916
+ recordTip.value = TUITranslateService.t("TUIChat.松手取消");
917
+ } else {
918
+ recordCancel.value = false;
919
+ recordTip.value = TUITranslateService.t("TUIChat.松开发送");
920
+ }
921
+ }
922
+ }
923
+
924
+ const updateRecordWave = () => {
925
+ if (!isRecording.value) return;
926
+ const dataArray = recorder.value.getRecordAnalyseData();
927
+ const chunkSize = Math.floor(dataArray.length / 24);
928
+ const newBars = [{height: 0}];
929
+ const SILENCE_THRESHOLD = 129;
930
+ for (let i = 0; i < 24; i++) {
931
+ const chunk = dataArray.slice(i * chunkSize, (i + 1) * chunkSize);
932
+ const avg = chunk.reduce((sum, val) => sum + Math.abs(val), 0) / chunkSize; // 使用绝对值确保正值
933
+ let height;
934
+ if (avg < SILENCE_THRESHOLD) {
935
+ // 静音时固定为5px
936
+ height = 5;
937
+ } else {
938
+ // 动态映射:静音(5px) ~ 最大音量(60px)
939
+ const normalized = Math.min(avg / 255, 1); // 限制到[0,1]范围
940
+ height = 5 + normalized * 55; // 5 + (0~1)*55 => 5~60px
941
+ }
942
+ newBars.push({ height });
943
+ }
944
+ audioWaveBars.value = newBars;
945
+ audioWaveAnimation = requestAnimationFrame(updateRecordWave);
946
+ }
947
+
948
+ const handleVisibilityChange = () => {
949
+ if (document.visibilityState === 'hidden') {
950
+ initRecorder();
951
+ }
952
+ }
716
953
  defineExpose({
717
954
  getEditorContent,
718
955
  addEmoji,
@@ -721,6 +958,8 @@ defineExpose({
721
958
  getEditorHTML,
722
959
  insertEditorContent,
723
960
  blur,
961
+ startRecording,
962
+ stopRecording
724
963
  });
725
964
  </script>
726
965
 
@@ -822,6 +1061,12 @@ defineExpose({
822
1061
  .message-input-editor-container-h5-no-emoji-no-tool {
823
1062
  border-radius: 10px;
824
1063
  }
1064
+ .message-input-editor-container-h5-audio {
1065
+ margin: 10px 0 0 5px;
1066
+ }
1067
+ .message-input-editor-container-h5-audio-mode {
1068
+ padding: 0;
1069
+ }
825
1070
  </style>
826
1071
  <style lang="scss">
827
1072
  /* stylelint-disable-next-line selector-class-pattern */
@@ -915,7 +1160,93 @@ defineExpose({
915
1160
  pointer-events: none;
916
1161
  }
917
1162
  }
918
-
1163
+ .audio-mode {
1164
+ text-align: center;
1165
+ font-size: 14px;
1166
+ color: rgb(0,0,0);
1167
+ padding: 8px 0 8px 10px !important;
1168
+ }
1169
+ .no-copy {
1170
+ user-select: none; /* 标准属性 */
1171
+ -webkit-user-select: none; /* Safari/Chrome */
1172
+ -moz-user-select: none; /* Firefox */
1173
+ -ms-user-select: none; /* IE10+ */
1174
+ }
1175
+ .record-container{
1176
+ height: 30vh;
1177
+ width: 100vw;
1178
+ position:fixed;
1179
+ overflow: hidden;
1180
+ right: 0;
1181
+ bottom: 0;
1182
+ background: linear-gradient(180deg, #FFF 0%, #FFF 64.35%);
1183
+ background: linear-gradient(180deg, color(display-p3 1 1 1 / 0.00) 0%, color(display-p3 1 1 1) 64.35%);
1184
+ display: flex;
1185
+ flex-direction: column;
1186
+ justify-content: flex-end;
1187
+ .audio-wave-container {
1188
+ box-sizing: border-box;
1189
+ width: 100vw;
1190
+ margin-bottom: 10px;
1191
+ display: flex;
1192
+ .audio-wave-bubble {
1193
+ background: #006AF6;
1194
+ margin: 0 10px;
1195
+ height: 34px;
1196
+ border-radius: 10px;
1197
+ width: 100%;
1198
+ display: flex;
1199
+ justify-content: center;
1200
+ align-items: center;
1201
+ }
1202
+ .audio-wave-bubble-cancel {
1203
+ background: #FF002F;
1204
+ }
1205
+ .audio-wave {
1206
+ color: white;
1207
+ display: flex;
1208
+ gap: 4px;
1209
+ .wave-bar {
1210
+ width: 3px;
1211
+ background: white;
1212
+ border-radius: 6px;
1213
+ transition: height 0.1s ease-out;
1214
+ margin-top: auto;
1215
+ margin-bottom: auto;
1216
+ transform-origin: center bottom;
1217
+ }
1218
+ }
1219
+ }
1220
+ .icon-container{
1221
+ position: absolute;
1222
+ bottom: -190%;
1223
+ left: -80%;
1224
+ width: 950px;
1225
+ height: 950px;
1226
+ border-radius: 50%;
1227
+ background: linear-gradient(180deg, #E8EDF3 -1.18%, #FBFBFB 4.94%);
1228
+ box-shadow: 0px -7.5px 5px 0px rgba(218, 224, 232, 0.15);
1229
+ }
1230
+ .record-tip{
1231
+ color:#656A72;
1232
+ font-size: 12px;
1233
+ margin-bottom: 12px;
1234
+ }
1235
+ .audio-record-icon{
1236
+ position:absolute;
1237
+ bottom:50px;
1238
+ left: 50%;
1239
+ transform: translateX(-50%);
1240
+ z-index:2;
1241
+ }
1242
+ .audio-close-icon {
1243
+ position:absolute;
1244
+ bottom:160px;
1245
+ left: 50%;
1246
+ transform: translateX(-50%);
1247
+ z-index:2;
1248
+ }
1249
+ }
919
1250
  .message-input-mute {
920
1251
  color: #721c24;
921
1252
  background-color: #f8d7da;
@@ -548,8 +548,21 @@ const onCurrentConversationIDUpdated = (conversationID: string) => {
548
548
  // Synchronize storage about whether the audio has been played when converstaion switched
549
549
  chatStorage.setChatStorage('audioPlayedMapping', audioPlayedMapping.value);
550
550
  }
551
+ preloadEmoji();
551
552
  };
552
553
 
554
+ const preloadEmoji = () => {
555
+ setTimeout(() => {
556
+ let url;
557
+ let img;
558
+ for (let i = 0; i <= 61; i++) {
559
+ url = `https://web.sdk.qcloud.com/im/assets/emoji-plugin/emoji_${i}@2x.png`;
560
+ img = new Image();
561
+ img.src = url;
562
+ }
563
+ }, 0);
564
+ }
565
+
553
566
  const getHistoryMessageList = () => {
554
567
  TUIChatService.getMessageList().then((res: any) => {
555
568
  const { nextReqMessageID: ID } = res.data;
@@ -9,24 +9,22 @@
9
9
  @click.stop="play"
10
10
  >
11
11
  <div class="audio-icon-container">
12
- <!-- <div :class="{ mask: true, play: isAudioPlaying }" /> -->
13
12
  <Icon
14
13
  :class="{icon:true,play: isAudioPlaying}"
15
14
  width="16px"
16
- height="20px"
17
- :file="audioIcon"
18
-
15
+ height="16px"
16
+ :file="currentAudioIcon"
19
17
  />
20
18
  </div>
21
19
  <span
22
20
  class="time"
23
- :style="{ width: `${data.second * 10 + 20}px` }"
21
+ :style="{ width: `${data.second * 2 + 20}px` }"
24
22
  >
25
- {{ data.second || 1 }} "
23
+ {{ data.second || 1 }}"
26
24
  </span>
27
25
  <audio
28
26
  ref="audioRef"
29
- :src="data.url"
27
+ :src="audioUrl"
30
28
  />
31
29
  </div>
32
30
  </template>
@@ -34,9 +32,16 @@
34
32
  <script lang="ts" setup>
35
33
  import vue from '../../../../adapter-vue';
36
34
  import Icon from '../../../common/Icon.vue';
37
- import audioIcon from '../../../../assets/msg-audio.svg';
35
+ import audioIcon1 from '../../../../assets/audio_icon_1.svg';
36
+ import audioIcon2 from '../../../../assets/audio_icon_2.svg';
37
+ import audioIcon3 from '../../../../assets/audio_icon_3.svg';
38
38
  import { isMobile } from '../../../../utils/env';
39
- const { watchEffect, ref, onMounted, onUnmounted } = vue;
39
+ import {
40
+ Toast,
41
+ TOAST_TYPE,
42
+ } from '../../../common/Toast/index-web';
43
+ import { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
44
+ const { watchEffect, ref, onMounted, onUnmounted, computed } = vue;
40
45
 
41
46
  interface IEmits {
42
47
  (e: 'setAudioPlayed', messageID: string): void;
@@ -58,6 +63,11 @@ const data = ref();
58
63
  const message = ref();
59
64
  const isAudioPlaying = ref();
60
65
  const audioRef = ref<HTMLAudioElement>();
66
+ const audioIcons = [audioIcon1, audioIcon2, audioIcon3];
67
+ const currentAudioIconIndex = ref(2);
68
+ const audioUrl = ref('');
69
+ const currentAudioIcon = computed(() => audioIcons[currentAudioIconIndex.value]);
70
+ let audioIconTimer: any = null;
61
71
 
62
72
  onMounted(() => {
63
73
  if (audioRef.value) {
@@ -76,6 +86,7 @@ onUnmounted(() => {
76
86
  watchEffect(() => {
77
87
  message.value = props.messageItem;
78
88
  data.value = props.content;
89
+ audioUrl.value = data.value.url || message.value.payload.remoteAudioUrl;
79
90
  });
80
91
 
81
92
  function play() {
@@ -95,45 +106,57 @@ function play() {
95
106
  audio.currentTime = 0;
96
107
  }
97
108
  });
98
- audioRef.value.play();
109
+ try {
110
+ if (!audioUrl.value) {
111
+ const message = `${TUITranslateService.t("TUIChat.语音播放失败")}`;
112
+ Toast({
113
+ message,
114
+ type: TOAST_TYPE.ERROR,
115
+ duration: 2000,
116
+ });
117
+ return;
118
+ }
119
+ audioRef.value.play();
120
+ if (!audioIconTimer) {
121
+ audioIconTimer = setInterval(() => {
122
+ currentAudioIconIndex.value = (currentAudioIconIndex.value + 1) % audioIcons.length;
123
+ }, 500);
124
+ }
125
+
126
+ } catch (e) {
127
+ console.warn(e);
128
+ }
99
129
  isAudioPlaying.value = true;
100
130
  if (message.value.flow === 'in') {
101
131
  emits('setAudioPlayed', message.value.ID);
102
132
  }
103
133
  }
104
134
 
135
+ function initAudioIcon() {
136
+ clearInterval(audioIconTimer);
137
+ audioIconTimer = null;
138
+ currentAudioIconIndex.value = 2;
139
+ }
140
+
105
141
  function onAudioEnded() {
106
142
  isAudioPlaying.value = false;
143
+ initAudioIcon();
107
144
  }
108
145
 
109
146
  function onAudioPaused() {
110
147
  isAudioPlaying.value = false;
148
+ initAudioIcon();
111
149
  }
112
150
  </script>
113
151
  <style lang="scss" scoped>
114
152
  @import "../../style/common";
115
-
116
- $flow-in-bg-color: #fbfbfb;
117
- $flow-out-bg-color: #dceafd;
118
-
119
- @keyframes blink {
120
- 0% {
121
- opacity: 1;
122
- }
123
- 50% {
124
- opacity: 0;
125
- }
126
- 100% {
127
- opacity: 1;
128
- }
129
- }
130
-
131
153
  .message-audio {
132
154
  flex-direction: row;
133
155
  display: flex;
134
156
  flex: 0 0 auto;
135
157
  cursor: pointer;
136
158
  overflow: hidden;
159
+ align-items: center;
137
160
 
138
161
  .time {
139
162
  flex: 1 1 auto;
@@ -156,47 +179,6 @@ $flow-out-bg-color: #dceafd;
156
179
  position: relative;
157
180
  margin: 0 7px 0 0;
158
181
  overflow: hidden;
159
-
160
- .mask {
161
- position: absolute;
162
- z-index: 2;
163
- width: 100%;
164
- height: 100%;
165
- left: 0;
166
- top: 0;
167
- transform-origin: right;
168
- transform: scaleX(0);
169
- background-color: $flow-in-bg-color;
170
-
171
- &.play {
172
- animation: audio-play 2s steps(1, end) infinite;
173
- }
174
- }
175
- .icon{
176
- &.play{
177
- animation: blink 1s infinite;
178
- }
179
- }
180
- }
181
-
182
- @keyframes audio-play {
183
- 0% {
184
- transform: scaleX(0.7056);
185
- }
186
-
187
- 50% {
188
- transform: scaleX(0.3953);
189
- }
190
-
191
- 75% {
192
- transform: scaleX(0);
193
- visibility: hidden;
194
- }
195
-
196
- 100% {
197
- transform: scaleX(0);
198
- visibility: hidden;
199
- }
200
182
  }
201
183
 
202
184
  .message-audio.reserve {
@@ -206,18 +188,8 @@ $flow-out-bg-color: #dceafd;
206
188
  text-align: end;
207
189
  }
208
190
 
209
- .icon {
210
- transform: rotate(180deg);
211
- }
212
-
213
191
  .audio-icon-container {
214
- margin: 0 0 0 7px;
215
-
216
- .mask {
217
- transform-origin: left;
218
- background-color: $flow-out-bg-color;
219
- // mask: linear-gradient(0deg, transparent 50%);
220
- }
192
+ margin: 0 0 0 4px;
221
193
  }
222
194
  }
223
195
  </style>
@@ -364,7 +364,7 @@ function onDislike(messageInfo: Object) {
364
364
  }
365
365
 
366
366
  .message-bubble {
367
- padding: 10px 5px;
367
+ padding: 10px;
368
368
  display: flex;
369
369
  flex-direction: row;
370
370
  user-select: none;