@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.
- package/CHANGELOG.md +13 -0
- package/assets/audio-blue.svg +4 -0
- package/assets/audio_icon_1.svg +3 -0
- package/assets/audio_icon_2.svg +3 -0
- package/assets/audio_icon_3.svg +3 -0
- package/assets/keyboard-icon.svg +9 -0
- package/components/CustomerServiceChat/index-web.vue +10 -2
- package/components/CustomerServiceChat/message-input/index-web.vue +57 -10
- package/components/CustomerServiceChat/message-input/message-input-editor-web.vue +342 -11
- package/components/CustomerServiceChat/message-list/index-web.vue +13 -0
- package/components/CustomerServiceChat/message-list/message-elements/message-audio-web.vue +50 -78
- package/components/CustomerServiceChat/message-list/message-elements/message-bubble-web.vue +1 -1
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/marked.ts +3 -2
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-robot-welcome.vue +13 -6
- package/components/CustomerServiceChat/message-list/message-elements/message-text.vue +3 -2
- package/components/common/Toast/index-web.ts +16 -3
- package/components/common/Toast/index-web.vue +16 -6
- package/constant.ts +1 -0
- package/locales/en/TUIChat.ts +6 -2
- package/locales/fil/TUIChat.ts +6 -2
- package/locales/id/TUIChat.ts +76 -72
- package/locales/ja/TUIChat.ts +76 -72
- package/locales/ms/TUIChat.ts +76 -72
- package/locales/ru/TUIChat.ts +6 -2
- package/locales/th/TUIChat.ts +76 -72
- package/locales/vi/TUIChat.ts +76 -72
- package/locales/zh_cn/TUIChat.ts +6 -2
- package/locales/zh_tw/TUIChat.ts +6 -2
- package/package.json +2 -1
- package/server.ts +22 -3
- 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.
|
|
5
|
+
isPC && !props.shouldShowToolbar && 'message-input-editor-container-no-inputTool',
|
|
6
6
|
isH5 && 'message-input-editor-container-h5',
|
|
7
|
-
isH5 && !props.
|
|
8
|
-
isH5 && !props.
|
|
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
|
-
|
|
119
|
+
isInAudioMode:{
|
|
120
|
+
type: Boolean,
|
|
121
|
+
default: false,
|
|
122
|
+
},
|
|
123
|
+
shouldShowEmoji: {
|
|
81
124
|
type: Boolean,
|
|
82
125
|
default: true,
|
|
83
126
|
},
|
|
84
|
-
|
|
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
|
-
|
|
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="
|
|
17
|
-
:file="
|
|
18
|
-
|
|
15
|
+
height="16px"
|
|
16
|
+
:file="currentAudioIcon"
|
|
19
17
|
/>
|
|
20
18
|
</div>
|
|
21
19
|
<span
|
|
22
20
|
class="time"
|
|
23
|
-
:style="{ width: `${data.second *
|
|
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="
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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>
|