@tencentcloud/ai-desk-customer-vue 1.0.0 → 1.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.
- package/CHANGELOG.md +14 -0
- package/README.md +177 -84
- package/assets/send_button_h5.svg +1 -0
- package/components/CustomerServiceChat/message-input/index-web.vue +16 -7
- package/components/CustomerServiceChat/message-input/message-input-editor-web.vue +5 -2
- package/components/CustomerServiceChat/message-list/index-web.vue +5 -0
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-desk.vue +11 -4
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-rating/message-rating-number.vue +2 -1
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-rating/message-rating-star.vue +2 -1
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-rich-text.vue +3 -5
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-stream.vue +65 -6
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/styles/common.scss +1 -1
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-plugin-web.vue +5 -0
- package/components/CustomerServiceChat/message-toolbar-button/index.vue +8 -2
- package/index.ts +3 -1
- package/package.json +1 -1
- package/server.ts +91 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.1.0] (2025-4-23)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
- 新增 `initWithProfile` 接口,支持用户端带昵称(nickName)和头像(avatar)登录,提升人工客服与用户交互体验。
|
|
5
|
+
- 优化流式消息和 markdown 图文混排的产品体验。
|
|
6
|
+
- 优化发消息体验。
|
|
7
|
+
- 优化组件样式。
|
|
8
|
+
|
|
9
|
+
## [1.0.1] (2025-4-16)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
- 简化接入。
|
|
13
|
+
- TUICustomerServer 新增 sendTextMessage/sendCustomMessage/changeLanguage 等接口。
|
|
14
|
+
|
|
1
15
|
## [1.0.0] (2025-4-10)
|
|
2
16
|
|
|
3
17
|
### Features
|
package/README.md
CHANGED
|
@@ -177,37 +177,11 @@ xcopy .\node_modules\@tencentcloud\ai-desk-customer-vue .\src\ai-desk-customer-v
|
|
|
177
177
|
<template>
|
|
178
178
|
<CustomerServiceChat
|
|
179
179
|
:style="{ width: '600px', height: '80vh', margin: '10px auto', boxShadow: '0 11px 20px #ccc' }"
|
|
180
|
-
:toolbarButtonList="toolbarButtonList"
|
|
181
180
|
/>
|
|
182
181
|
</template>
|
|
183
182
|
<script setup lang="ts">
|
|
184
|
-
import TUICustomerServer from './ai-desk-customer-vue
|
|
185
|
-
import {
|
|
186
|
-
import { onMounted, ref } from "vue";
|
|
187
|
-
import TUIChatEngine, { TUIChatService } from "@tencentcloud/chat-uikit-engine";
|
|
188
|
-
|
|
189
|
-
// 配置工具栏 button list,例如【转人工】
|
|
190
|
-
const toolbarButtonList = ref([
|
|
191
|
-
{
|
|
192
|
-
title: '转人工',
|
|
193
|
-
renderCondition: () => {
|
|
194
|
-
// return true 即渲染,您可以在这里控制 button 显隐时机
|
|
195
|
-
return true;
|
|
196
|
-
},
|
|
197
|
-
clickEvent: () => {
|
|
198
|
-
// 点击 button 后的回调
|
|
199
|
-
if (TUIChatEngine.isReady()) {
|
|
200
|
-
TUIChatService.sendTextMessage({
|
|
201
|
-
to: '@customer_service_account',
|
|
202
|
-
conversationType: TUIChatEngine.TYPES.CONV_C2C,
|
|
203
|
-
payload: {
|
|
204
|
-
text: '转人工'
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
]);
|
|
183
|
+
import TUICustomerServer, { CustomerServiceChat } from './ai-desk-customer-vue';
|
|
184
|
+
import { onMounted } from "vue";
|
|
211
185
|
|
|
212
186
|
onMounted(() => {
|
|
213
187
|
const SDKAppID = 0; // Your SDKAppID,即开通了智能客服 Desk 的应用 ID
|
|
@@ -226,40 +200,14 @@ onMounted(() => {
|
|
|
226
200
|
<div id="app">
|
|
227
201
|
<CustomerServiceChat
|
|
228
202
|
:style="{ width: '600px', height: '80vh', margin: '10px auto', boxShadow: '0 11px 20px #ccc' }"
|
|
229
|
-
:toolbarButtonList="toolbarButtonList"
|
|
230
203
|
/>
|
|
231
204
|
</div>
|
|
232
205
|
</template>
|
|
233
206
|
<script lang="ts" setup>
|
|
234
|
-
import TUICustomerServer from './ai-desk-customer-vue
|
|
235
|
-
import { CustomerServiceChat } from './ai-desk-customer-vue';
|
|
207
|
+
import TUICustomerServer, { CustomerServiceChat } from './ai-desk-customer-vue';
|
|
236
208
|
import vue from './ai-desk-customer-vue/adapter-vue';
|
|
237
|
-
import TUIChatEngine, { TUIChatService } from "@tencentcloud/chat-uikit-engine";
|
|
238
|
-
|
|
239
|
-
const { ref, onMounted } = vue;
|
|
240
209
|
|
|
241
|
-
|
|
242
|
-
const toolbarButtonList = ref([
|
|
243
|
-
{
|
|
244
|
-
title: '转人工',
|
|
245
|
-
renderCondition: () => {
|
|
246
|
-
// return true 即渲染,您可以在这里控制 button 显隐时机
|
|
247
|
-
return true;
|
|
248
|
-
},
|
|
249
|
-
clickEvent: () => {
|
|
250
|
-
// 点击 button 后的回调
|
|
251
|
-
if (TUIChatEngine.isReady()) {
|
|
252
|
-
TUIChatService.sendTextMessage({
|
|
253
|
-
to: '@customer_service_account',
|
|
254
|
-
conversationType: TUIChatEngine.TYPES.CONV_C2C,
|
|
255
|
-
payload: {
|
|
256
|
-
text: '转人工'
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
]);
|
|
210
|
+
const { onMounted } = vue;
|
|
263
211
|
|
|
264
212
|
onMounted(() => {
|
|
265
213
|
const SDKAppID = 0; // Your SDKAppID
|
|
@@ -278,40 +226,14 @@ onMounted(() => {
|
|
|
278
226
|
<div id="app">
|
|
279
227
|
<CustomerServiceChat
|
|
280
228
|
:style="{ width: '600px', height: '80vh', margin: '10px auto', boxShadow: '0 11px 20px #ccc' }"
|
|
281
|
-
:toolbarButtonList="toolbarButtonList"
|
|
282
229
|
/>
|
|
283
230
|
</div>
|
|
284
231
|
</template>
|
|
285
232
|
<script lang="ts" setup>
|
|
286
|
-
import TUICustomerServer from './ai-desk-customer-vue
|
|
287
|
-
import { CustomerServiceChat } from './ai-desk-customer-vue';
|
|
233
|
+
import TUICustomerServer, { CustomerServiceChat } from './ai-desk-customer-vue';
|
|
288
234
|
import vue from './ai-desk-customer-vue/adapter-vue';
|
|
289
|
-
import TUIChatEngine, { TUIChatService } from "@tencentcloud/chat-uikit-engine";
|
|
290
235
|
|
|
291
|
-
const {
|
|
292
|
-
|
|
293
|
-
// 配置工具栏 button list,例如【转人工】
|
|
294
|
-
const toolbarButtonList = ref([
|
|
295
|
-
{
|
|
296
|
-
title: '转人工',
|
|
297
|
-
renderCondition: () => {
|
|
298
|
-
// return true 即渲染,您可以在这里控制 button 显隐时机
|
|
299
|
-
return true;
|
|
300
|
-
},
|
|
301
|
-
clickEvent: () => {
|
|
302
|
-
// 点击 button 后的回调
|
|
303
|
-
if (TUIChatEngine.isReady()) {
|
|
304
|
-
TUIChatService.sendTextMessage({
|
|
305
|
-
to: '@customer_service_account',
|
|
306
|
-
conversationType: TUIChatEngine.TYPES.CONV_C2C,
|
|
307
|
-
payload: {
|
|
308
|
-
text: '转人工'
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
]);
|
|
236
|
+
const { onMounted } = vue;
|
|
315
237
|
|
|
316
238
|
onMounted(() => {
|
|
317
239
|
const SDKAppID = 0; // Your SDKAppID
|
|
@@ -433,6 +355,177 @@ npm run serve
|
|
|
433
355
|
npm run dev
|
|
434
356
|
```
|
|
435
357
|
|
|
358
|
+
## 高级特性
|
|
359
|
+
|
|
360
|
+
### 国际化界面语言
|
|
361
|
+
|
|
362
|
+
从 v1.0.0 起,UIKit 支持以下界面语言:
|
|
363
|
+
<table>
|
|
364
|
+
<tr>
|
|
365
|
+
<td rowspan="1" colSpan="1" >语言代码(userLang)</td>
|
|
366
|
+
|
|
367
|
+
<td rowspan="1" colSpan="1" >语言</td>
|
|
368
|
+
</tr>
|
|
369
|
+
|
|
370
|
+
<tr>
|
|
371
|
+
<td rowspan="1" colSpan="1" >zh_cn</td>
|
|
372
|
+
|
|
373
|
+
<td rowspan="1" colSpan="1" >简体中文</td>
|
|
374
|
+
</tr>
|
|
375
|
+
|
|
376
|
+
<tr>
|
|
377
|
+
<td rowspan="1" colSpan="1" >en</td>
|
|
378
|
+
|
|
379
|
+
<td rowspan="1" colSpan="1" >英文</td>
|
|
380
|
+
</tr>
|
|
381
|
+
|
|
382
|
+
<tr>
|
|
383
|
+
<td rowspan="1" colSpan="1" >zh_tw</td>
|
|
384
|
+
|
|
385
|
+
<td rowspan="1" colSpan="1" >繁体中文</td>
|
|
386
|
+
</tr>
|
|
387
|
+
|
|
388
|
+
<tr>
|
|
389
|
+
<td rowspan="1" colSpan="1" >ja</td>
|
|
390
|
+
|
|
391
|
+
<td rowspan="1" colSpan="1" >日语</td>
|
|
392
|
+
</tr>
|
|
393
|
+
|
|
394
|
+
<tr>
|
|
395
|
+
<td rowspan="1" colSpan="1" >id</td>
|
|
396
|
+
|
|
397
|
+
<td rowspan="1" colSpan="1" >印尼语</td>
|
|
398
|
+
</tr>
|
|
399
|
+
|
|
400
|
+
<tr>
|
|
401
|
+
<td rowspan="1" colSpan="1" >ms</td>
|
|
402
|
+
|
|
403
|
+
<td rowspan="1" colSpan="1" >马来语</td>
|
|
404
|
+
</tr>
|
|
405
|
+
|
|
406
|
+
<tr>
|
|
407
|
+
<td rowspan="1" colSpan="1" >vi</td>
|
|
408
|
+
|
|
409
|
+
<td rowspan="1" colSpan="1" >越南语</td>
|
|
410
|
+
</tr>
|
|
411
|
+
|
|
412
|
+
<tr>
|
|
413
|
+
<td rowspan="1" colSpan="1" >th</td>
|
|
414
|
+
|
|
415
|
+
<td rowspan="1" colSpan="1" >泰语</td>
|
|
416
|
+
</tr>
|
|
417
|
+
|
|
418
|
+
<tr>
|
|
419
|
+
<td rowspan="1" colSpan="1" >fil</td>
|
|
420
|
+
|
|
421
|
+
<td rowspan="1" colSpan="1" >菲律宾语</td>
|
|
422
|
+
</tr>
|
|
423
|
+
|
|
424
|
+
<tr>
|
|
425
|
+
<td rowspan="1" colSpan="1" >ru</td>
|
|
426
|
+
|
|
427
|
+
<td rowspan="1" colSpan="1" >俄语</td>
|
|
428
|
+
</tr>
|
|
429
|
+
</table>
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
如果您的业务需要出海,且用户语言以英语为主,可在引入智能客服时设置`userLang="en"`。如果您不指定`userLang`, UIKit 会使用浏览器设置的语言。
|
|
433
|
+
``` javascript
|
|
434
|
+
<template>
|
|
435
|
+
<CustomerServiceChat style="height: 100%;"
|
|
436
|
+
userLang="en"
|
|
437
|
+
/>
|
|
438
|
+
</template>
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
如果您需要支持动态切换用户语言,可使用`TUICustomerServer.changeLanguage`接口,并通过切换 页面/组件 key 的方式,实现语言动态修改与展示。
|
|
442
|
+
``` javascript
|
|
443
|
+
<template>
|
|
444
|
+
<CustomerServiceChat style="height: 100%;"
|
|
445
|
+
:key="locale"
|
|
446
|
+
:userLang="locale"
|
|
447
|
+
/>
|
|
448
|
+
</template>
|
|
449
|
+
<script setup lang="ts">
|
|
450
|
+
import TUICustomerServer, { CustomerServiceChat } from '../../ai-desk-customer-uniapp';
|
|
451
|
+
import { onMounted, ref } from "vue";
|
|
452
|
+
|
|
453
|
+
const locale = ref('en');
|
|
454
|
+
|
|
455
|
+
const changeLanguage = (language: string) => {
|
|
456
|
+
TUICustomerServer.changeLanguage(language).then(() => {
|
|
457
|
+
locale.value = language;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
</script>
|
|
461
|
+
<style scoped lang="scss">
|
|
462
|
+
</style>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### 工具栏快捷 button
|
|
466
|
+
|
|
467
|
+
如果您想实现输入框上方增加快捷 button,方便用户使用,比如增加“人工客服”,“发送订单消息”等,可在引入智能客服时设置 `toolbarButtonList`。效果如下所示:
|
|
468
|
+
|
|
469
|
+

|
|
470
|
+
|
|
471
|
+
``` javascript
|
|
472
|
+
<template>
|
|
473
|
+
<CustomerServiceChat style="height: 100%;"
|
|
474
|
+
:toolbarButtonList="toolbarButtonList"
|
|
475
|
+
/>
|
|
476
|
+
</template>
|
|
477
|
+
<script setup lang="ts">
|
|
478
|
+
import TUICustomerServer, { CustomerServiceChat } from '../../ai-desk-customer-uniapp';
|
|
479
|
+
import { ref } from "vue";
|
|
480
|
+
|
|
481
|
+
const toolbarButtonList = ref([
|
|
482
|
+
{
|
|
483
|
+
title: '人工客服',
|
|
484
|
+
renderCondition: () => {
|
|
485
|
+
return true;
|
|
486
|
+
},
|
|
487
|
+
clickEvent: () => {
|
|
488
|
+
// 点击 button 后的回调
|
|
489
|
+
TUICustomerServer.sendTextMessage({
|
|
490
|
+
to: '@customer_service_account',
|
|
491
|
+
conversationType: 'C2C',
|
|
492
|
+
payload: {
|
|
493
|
+
text: '人工客服'
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
title: '发送订单',
|
|
500
|
+
renderCondition: () => {
|
|
501
|
+
return true;
|
|
502
|
+
},
|
|
503
|
+
clickEvent: () => {
|
|
504
|
+
// 点击 button 后的回调
|
|
505
|
+
TUICustomerServer.sendCustomMessage({
|
|
506
|
+
to: '@customer_service_account',
|
|
507
|
+
conversationType: 'C2C',
|
|
508
|
+
payload: {
|
|
509
|
+
data: JSON.stringify({
|
|
510
|
+
src: '22',
|
|
511
|
+
customerServicePlugin: 0,
|
|
512
|
+
content: {
|
|
513
|
+
desc: "¥3000/月",
|
|
514
|
+
header: "高级版智能客服",
|
|
515
|
+
pic: "https://cloudcache.tencent-cloud.com/qcloud/portal/kit/images/presale.a4955999.jpeg",
|
|
516
|
+
url: "https://cloud.tencent.com/document/product/269/116070"
|
|
517
|
+
}
|
|
518
|
+
}),
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
]);
|
|
524
|
+
</script>
|
|
525
|
+
<style scoped lang="scss">
|
|
526
|
+
</style>
|
|
527
|
+
```
|
|
528
|
+
|
|
436
529
|
## 常见问题
|
|
437
530
|
|
|
438
531
|
##### 什么是 UserSig?如何生成 UserSig?
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1C66E5"><path d="M440-320h80v-168l64 64 56-56-160-160-160 160 56 56 64-64v168Zm40 240q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg>
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
@sendMessage="sendMessage"
|
|
19
19
|
@onTyping="onTyping"
|
|
20
20
|
@blurToolAndEmojiH5="blurToolAndEmojiH5"
|
|
21
|
+
@isInputNotEmpty="isInputNotEmpty"
|
|
21
22
|
/>
|
|
22
23
|
<MessageInputButton
|
|
23
24
|
v-if="!props.isMuted && isPC"
|
|
@@ -26,9 +27,12 @@
|
|
|
26
27
|
<div v-if="isH5" class="emoji-icon" id="emoji-icon-h5" @click="emojiShow">
|
|
27
28
|
<Icon :file="emojiIcon" width="24px" height="24px"/>
|
|
28
29
|
</div>
|
|
29
|
-
<div v-
|
|
30
|
+
<div v-show="isH5 && !showSendButton" class="tool-icon" @click="toolShow">
|
|
30
31
|
<Icon :file="toolIcon" width="24px" height="24px"/>
|
|
31
32
|
</div>
|
|
33
|
+
<div v-show="isH5 && showSendButton" class="send-button-h5" @click="sendMessage">
|
|
34
|
+
<Icon :file="sendButtonIcon" width="34px" height="34px"/>
|
|
35
|
+
</div>
|
|
32
36
|
</div>
|
|
33
37
|
|
|
34
38
|
</div>
|
|
@@ -50,6 +54,7 @@ import { isPC,isH5 } from '../../../utils/env';
|
|
|
50
54
|
import Icon from '../../common/Icon.vue';
|
|
51
55
|
import emojiIcon from '../../../assets/emoji.png';
|
|
52
56
|
import toolIcon from '../../../assets/more_tools.png';
|
|
57
|
+
import sendButtonIcon from '../../../assets/send_button_h5.svg';
|
|
53
58
|
const { ref,onMounted,onBeforeUnmount } = vue;
|
|
54
59
|
|
|
55
60
|
const props = defineProps({
|
|
@@ -83,6 +88,7 @@ const emit = defineEmits(['sendMessage', 'resetReplyOrReference', 'onTyping','sc
|
|
|
83
88
|
const editor = ref<InstanceType<typeof MessageInputEditor>>();
|
|
84
89
|
const currentConversation = ref<IConversationModel>();
|
|
85
90
|
const h5Dialog = ref<HTMLElement>();
|
|
91
|
+
const showSendButton = ref(false);
|
|
86
92
|
|
|
87
93
|
onMounted(() => {
|
|
88
94
|
// document.addEventListener('click', handleClick);
|
|
@@ -126,17 +132,23 @@ const sendMessage = async () => {
|
|
|
126
132
|
await sendMessages(editorContentList, currentConversation.value);
|
|
127
133
|
emit('sendMessage');
|
|
128
134
|
editor.value?.resetEditor();
|
|
135
|
+
showSendButton.value = false;
|
|
129
136
|
};
|
|
130
137
|
|
|
131
138
|
const insertEmoji = (emoji: any) => {
|
|
132
139
|
editor.value?.addEmoji(emoji);
|
|
140
|
+
showSendButton.value = true;
|
|
133
141
|
};
|
|
134
142
|
|
|
135
143
|
const reEdit = (content: any) => {
|
|
136
144
|
editor.value?.resetEditor();
|
|
145
|
+
showSendButton.value = false;
|
|
137
146
|
editor.value?.setEditorContent(content);
|
|
138
147
|
};
|
|
139
148
|
|
|
149
|
+
const isInputNotEmpty = (show: boolean) => {
|
|
150
|
+
showSendButton.value = show;
|
|
151
|
+
}
|
|
140
152
|
|
|
141
153
|
const emojiShow = () => {
|
|
142
154
|
emit('emojiShow');
|
|
@@ -205,11 +217,8 @@ defineExpose({
|
|
|
205
217
|
height: fit-content;
|
|
206
218
|
background: #fff;
|
|
207
219
|
}
|
|
208
|
-
.send-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
padding: 10px 12px;
|
|
212
|
-
margin-left: 5px;
|
|
213
|
-
border-radius: 7px;
|
|
220
|
+
.send-button-h5 {
|
|
221
|
+
margin-right: 10px;
|
|
222
|
+
margin-left: 5px;
|
|
214
223
|
}
|
|
215
224
|
</style>
|
|
@@ -76,7 +76,7 @@ const props = defineProps({
|
|
|
76
76
|
},
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
const emits = defineEmits(['sendMessage', 'onTyping','blurToolAndEmojiH5']);
|
|
79
|
+
const emits = defineEmits(['sendMessage', 'onTyping', 'blurToolAndEmojiH5', 'isInputNotEmpty']);
|
|
80
80
|
const { placeholder, enableDragUpload, enableTyping } = toRefs(props);
|
|
81
81
|
const isEditorEmpty = ref<boolean>(true);
|
|
82
82
|
const isEditorBlur = ref<boolean>(true);
|
|
@@ -214,9 +214,12 @@ function handleEnter(e: any) {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
function handleH5Input() {
|
|
217
|
+
function handleH5Input(e: any) {
|
|
218
218
|
if (isH5) {
|
|
219
219
|
isEditorEmpty.value = editorDom.value?.childNodes ? false : true;
|
|
220
|
+
const hasText = e.target.innerText.trim().length > 0;
|
|
221
|
+
const hasImages = e.target.querySelectorAll('img.custom-image-emoji').length > 0;
|
|
222
|
+
emits('isInputNotEmpty', hasText || hasImages);
|
|
220
223
|
}
|
|
221
224
|
}
|
|
222
225
|
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
@resendMessage="resendMessage"
|
|
48
48
|
@handleToggleMessageItem="handleToggleMessageItem"
|
|
49
49
|
@handleH5LongPress="handleH5LongPress"
|
|
50
|
+
@heightChanged="onHeightChanged"
|
|
50
51
|
/>
|
|
51
52
|
<div
|
|
52
53
|
v-else
|
|
@@ -572,6 +573,10 @@ const handleH5LongPress = (e: any, message: IMessageModel, type: string) => {
|
|
|
572
573
|
}
|
|
573
574
|
};
|
|
574
575
|
|
|
576
|
+
function onHeightChanged() {
|
|
577
|
+
scrollToLatestMessage();
|
|
578
|
+
}
|
|
579
|
+
|
|
575
580
|
// re-edit message
|
|
576
581
|
const handleEdit = (message: IMessageModel) => {
|
|
577
582
|
emits('handleEditor', message, 'reedit');
|
|
@@ -38,7 +38,10 @@
|
|
|
38
38
|
<MessageRichText :payload="payload" />
|
|
39
39
|
</div>
|
|
40
40
|
<div v-if="payload.src === CUSTOM_MESSAGE_SRC.STREAM_TEXT">
|
|
41
|
-
<MessageStream
|
|
41
|
+
<MessageStream
|
|
42
|
+
:payload="payload"
|
|
43
|
+
@heightChanged="onHeightChanged"
|
|
44
|
+
/>
|
|
42
45
|
</div>
|
|
43
46
|
<div v-if="payload.src === CUSTOM_MESSAGE_SRC.MULTI_BRANCH">
|
|
44
47
|
<MessageMultiBranch :payload="payload" @sendMessage="sendTextMessage"/>
|
|
@@ -98,7 +101,7 @@ export default {
|
|
|
98
101
|
default: () => ({}),
|
|
99
102
|
},
|
|
100
103
|
},
|
|
101
|
-
emits: ['showFormPopup'],
|
|
104
|
+
emits: ['showFormPopup', 'heightChanged'],
|
|
102
105
|
setup(props: Props, { emit }) {
|
|
103
106
|
const payload = computed<customerServicePayloadType>(() => {
|
|
104
107
|
return props.message && JSONToObject(props.message?.payload?.data);
|
|
@@ -111,7 +114,10 @@ export default {
|
|
|
111
114
|
};
|
|
112
115
|
const handleShowFormPopup = (data: boolean) => {
|
|
113
116
|
emit('showFormPopup', data);
|
|
114
|
-
}
|
|
117
|
+
};
|
|
118
|
+
const onHeightChanged = () => {
|
|
119
|
+
emit('heightChanged');
|
|
120
|
+
};
|
|
115
121
|
|
|
116
122
|
return {
|
|
117
123
|
payload,
|
|
@@ -119,7 +125,8 @@ export default {
|
|
|
119
125
|
sendTextMessage,
|
|
120
126
|
CUSTOM_MESSAGE_SRC,
|
|
121
127
|
sendCustomMessage,
|
|
122
|
-
handleShowFormPopup
|
|
128
|
+
handleShowFormPopup,
|
|
129
|
+
onHeightChanged,
|
|
123
130
|
};
|
|
124
131
|
},
|
|
125
132
|
};
|
|
@@ -204,13 +204,14 @@ export default {
|
|
|
204
204
|
|
|
205
205
|
.submit-button {
|
|
206
206
|
height: 50px;
|
|
207
|
-
background-color: #
|
|
207
|
+
background-color: #1c66e5;
|
|
208
208
|
font-size: 18px;
|
|
209
209
|
font-weight: 400;
|
|
210
210
|
color: white;
|
|
211
211
|
border: 0;
|
|
212
212
|
border-radius: 8px;
|
|
213
213
|
cursor: pointer;
|
|
214
|
+
width: 62%;
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
.de-active {
|
|
@@ -225,13 +225,14 @@ export default {
|
|
|
225
225
|
|
|
226
226
|
.submit-button {
|
|
227
227
|
height: 35px;
|
|
228
|
-
background-color: #
|
|
228
|
+
background-color: #1c66e5;
|
|
229
229
|
font-size: 14px;
|
|
230
230
|
font-weight: 400;
|
|
231
231
|
color: white;
|
|
232
232
|
border: 0;
|
|
233
233
|
border-radius: 8px;
|
|
234
234
|
cursor: pointer;
|
|
235
|
+
width: 62%;
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
</style>
|
|
@@ -70,7 +70,7 @@ export default {
|
|
|
70
70
|
z-index: 101;
|
|
71
71
|
width: 100vw;
|
|
72
72
|
height: 100vh;
|
|
73
|
-
background: rgba(#000, 0.
|
|
73
|
+
background: rgba(#000, 0.8);
|
|
74
74
|
top: 0;
|
|
75
75
|
left: 0;
|
|
76
76
|
display: flex;
|
|
@@ -81,14 +81,12 @@ export default {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
.rich-image-preview {
|
|
84
|
-
width:
|
|
85
|
-
height:auto;
|
|
84
|
+
max-width: 90%;
|
|
85
|
+
height: auto;
|
|
86
86
|
display: flex;
|
|
87
87
|
align-items: center;
|
|
88
88
|
justify-content: center;
|
|
89
89
|
overflow: hidden;
|
|
90
|
-
transition: transform 0.1s ease 0s;
|
|
91
90
|
pointer-events: auto;
|
|
92
91
|
}
|
|
93
|
-
|
|
94
92
|
</style>
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="message-stream">
|
|
3
|
-
<pre :class="['message-marked']" v-html="displayedContent" />
|
|
3
|
+
<pre ref="preRef" :class="['message-marked']" v-html="displayedContent" />
|
|
4
|
+
</div>
|
|
5
|
+
<div v-if="image" class="rich-image-previewer" @click="closeImage">
|
|
6
|
+
<img
|
|
7
|
+
class="rich-image-preview"
|
|
8
|
+
:src="imageSrc"
|
|
9
|
+
/>
|
|
4
10
|
</div>
|
|
5
11
|
</template>
|
|
6
12
|
|
|
@@ -11,7 +17,7 @@ import { parseMarkdown } from './marked'
|
|
|
11
17
|
import { TypeWriter } from "./type-writer";
|
|
12
18
|
import { JSONToObject } from "../../../../../../utils";
|
|
13
19
|
|
|
14
|
-
const { ref, computed, withDefaults, defineProps, watch } = vue;
|
|
20
|
+
const { ref, computed, withDefaults, defineProps, watch, onMounted, onUnmounted } = vue;
|
|
15
21
|
|
|
16
22
|
interface Props {
|
|
17
23
|
payload: customerServicePayloadType;
|
|
@@ -22,11 +28,27 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
22
28
|
});
|
|
23
29
|
|
|
24
30
|
const isStreaming = ref<boolean>(false);
|
|
31
|
+
const image = ref(false);
|
|
32
|
+
const imageSrc = ref('');
|
|
25
33
|
const chunks = ref<string>('');
|
|
26
34
|
const isFinished = ref<boolean>(true);
|
|
27
35
|
const prevChunksLength = ref<number>(0);
|
|
28
36
|
const streamContent = ref<string>('');
|
|
29
|
-
const displayedContent = computed(() =>
|
|
37
|
+
const displayedContent = computed(() => {
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
window.onMarkdownImageClicked = function(href:string) {
|
|
40
|
+
image.value = !image.value;
|
|
41
|
+
imageSrc.value = decodeURIComponent(href);
|
|
42
|
+
}
|
|
43
|
+
return parseMarkdown(streamContent.value);
|
|
44
|
+
});
|
|
45
|
+
const preRef = ref();
|
|
46
|
+
const emits = defineEmits(['heightChanged']);
|
|
47
|
+
// 不支持 ResizeObserver 的浏览器太老旧,默认不处理了
|
|
48
|
+
const canIUseResizeObserver = typeof ResizeObserver === 'undefined' ? false : true;
|
|
49
|
+
|
|
50
|
+
let observer;
|
|
51
|
+
let prevHeight;
|
|
30
52
|
|
|
31
53
|
const typeWriter = new TypeWriter({
|
|
32
54
|
onTyping: (item: string) => {
|
|
@@ -60,15 +82,52 @@ watch(() => props.payload, (newValue: string, oldValue: string) => {
|
|
|
60
82
|
// disable typeWriter style or history message first load
|
|
61
83
|
streamContent.value = chunks.value;
|
|
62
84
|
} else {
|
|
63
|
-
|
|
64
|
-
|
|
85
|
+
// 判断长度是为了防御编辑的内容回退的异常 case
|
|
86
|
+
if (chunks.value.length > prevChunksLength.value) {
|
|
87
|
+
const _newChunksToAdd = chunks.value?.slice(prevChunksLength.value);
|
|
88
|
+
prevChunksLength.value = chunks.value.length;
|
|
89
|
+
startStreaming([_newChunksToAdd]);
|
|
90
|
+
}
|
|
65
91
|
}
|
|
66
|
-
prevChunksLength.value = chunks.value?.length;
|
|
67
92
|
}, {
|
|
68
93
|
deep: true,
|
|
69
94
|
immediate: true,
|
|
70
95
|
},
|
|
71
96
|
);
|
|
97
|
+
|
|
98
|
+
onMounted(() => {
|
|
99
|
+
if (canIUseResizeObserver) {
|
|
100
|
+
observer = new ResizeObserver(entries => {
|
|
101
|
+
for (let entry of entries) {
|
|
102
|
+
observeHeightChanged(entry.contentRect.height);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// 开始观察
|
|
106
|
+
observer.observe(preRef.value);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
onUnmounted(() => {
|
|
111
|
+
if (canIUseResizeObserver) {
|
|
112
|
+
if (observer) {
|
|
113
|
+
observer.disconnect();
|
|
114
|
+
observer = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const observeHeightChanged = (newHeight) => {
|
|
120
|
+
if (prevHeight !== newHeight) {
|
|
121
|
+
prevHeight = newHeight;
|
|
122
|
+
emits('heightChanged');
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const closeImage = () => {
|
|
127
|
+
image.value = !image.value;
|
|
128
|
+
imageSrc.value = '';
|
|
129
|
+
};
|
|
130
|
+
|
|
72
131
|
</script>
|
|
73
132
|
<style lang="scss" scoped>
|
|
74
133
|
.message-stream {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<MessageCustomerService
|
|
14
14
|
v-if="pluginMessageType.pluginType === 'customer'"
|
|
15
15
|
:message="props.message"
|
|
16
|
+
@heightChanged="onHeightChanged"
|
|
16
17
|
/>
|
|
17
18
|
</template>
|
|
18
19
|
</MessagePluginLayout>
|
|
@@ -44,6 +45,7 @@ const emits = defineEmits([
|
|
|
44
45
|
'resendMessage',
|
|
45
46
|
'handleToggleMessageItem',
|
|
46
47
|
'handleH5LongPress',
|
|
48
|
+
'heightChanged',
|
|
47
49
|
]);
|
|
48
50
|
const messageModel = computed(() => TUIStore.getMessageModel(props.message.ID));
|
|
49
51
|
|
|
@@ -76,6 +78,9 @@ const handleToggleMessageItem = (
|
|
|
76
78
|
const handleH5LongPress = (e: any, message: IMessageModel, type: string) => {
|
|
77
79
|
emits('handleH5LongPress', e, message, type);
|
|
78
80
|
};
|
|
81
|
+
const onHeightChanged = () => {
|
|
82
|
+
emits('heightChanged');
|
|
83
|
+
};
|
|
79
84
|
</script>
|
|
80
85
|
|
|
81
86
|
<style lang="scss" scoped>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="toolbar-button-container">
|
|
3
3
|
<template v-for="(item, index) in props.toolbarButtonList">
|
|
4
|
-
<div v-if="item
|
|
4
|
+
<div v-if="shouldRender(item)" :key="index"
|
|
5
5
|
:class="['toolbar-button', isH5 ? 'toolbar-button-h5' : '']" @click="onClick(index)">
|
|
6
6
|
<Icon v-if="item.icon" class="toolbar-button-icon" :file="item.icon" width="18px" height="18px"/>
|
|
7
7
|
<div class="toolbar-button-text">
|
|
@@ -26,7 +26,13 @@ const props = withDefaults(defineProps<IProps>(), {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
function onClick(index: number) {
|
|
29
|
-
props.toolbarButtonList[index].clickEvent
|
|
29
|
+
if (typeof props.toolbarButtonList[index].clickEvent === 'function') {
|
|
30
|
+
props.toolbarButtonList[index].clickEvent();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shouldRender(item: ToolbarButtonModel) {
|
|
35
|
+
return typeof item.renderCondition === 'function' ? item.renderCondition() : false;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
</script>
|
package/index.ts
CHANGED
package/package.json
CHANGED
package/server.ts
CHANGED
|
@@ -9,22 +9,36 @@ import TUIChatEngine, {
|
|
|
9
9
|
TUIChatService,
|
|
10
10
|
TUIConversationService,
|
|
11
11
|
IMessageModel,
|
|
12
|
-
TUITranslateService
|
|
12
|
+
TUITranslateService,
|
|
13
|
+
SendMessageParams,
|
|
14
|
+
SendMessageOptions,
|
|
15
|
+
TUIUserService,
|
|
13
16
|
} from '@tencentcloud/chat-uikit-engine';
|
|
14
17
|
import Log from './utils/logger';
|
|
15
18
|
import { version } from './package.json'
|
|
16
19
|
import { Toast, TOAST_TYPE } from "./components/common/Toast/index-web";
|
|
17
20
|
|
|
21
|
+
interface IInitWithProfile {
|
|
22
|
+
SDKAppID: number,
|
|
23
|
+
userID: string,
|
|
24
|
+
userSig: string,
|
|
25
|
+
nickName?: string,
|
|
26
|
+
avatar?: string,
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
export default class TUICustomerServer {
|
|
19
|
-
|
|
30
|
+
private isLoggedIn: boolean;
|
|
20
31
|
static instance: TUICustomerServer;
|
|
21
32
|
private customerServiceAccounts: any[];
|
|
22
|
-
|
|
33
|
+
private loggedInUserID: string;
|
|
34
|
+
private myProfile: object;
|
|
23
35
|
constructor() {
|
|
24
36
|
TUICore.registerService(TUIConstants.TUICustomerServicePlugin.SERVICE.NAME, this);
|
|
25
37
|
TUICore.registerExtension(TUIConstants.TUIContact.EXTENSION.CONTACT_LIST.EXT_ID, this);
|
|
26
38
|
this.customerServiceAccounts = ['@customer_service_account'];
|
|
27
|
-
|
|
39
|
+
this.isLoggedIn = false;
|
|
40
|
+
this.loggedInUserID = '';
|
|
41
|
+
this.myProfile = {};
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
static getInstance(): TUICustomerServer {
|
|
@@ -34,7 +48,7 @@ export default class TUICustomerServer {
|
|
|
34
48
|
return TUICustomerServer.instance;
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
private loginCustomerUIKit(SDKAppID:number, userID:string, userSig:string) {
|
|
38
52
|
clearChatStorage(SDKAppID, userID);
|
|
39
53
|
TUIChatEngine.login({
|
|
40
54
|
SDKAppID,
|
|
@@ -43,41 +57,71 @@ export default class TUICustomerServer {
|
|
|
43
57
|
useUploadPlugin: true,
|
|
44
58
|
}).then(() => {
|
|
45
59
|
Log.i(`login success. userID:${userID}`);
|
|
46
|
-
|
|
60
|
+
this.isLoggedIn = true;
|
|
61
|
+
this.loggedInUserID = userID;
|
|
47
62
|
TUIConversationService.switchConversation('C2C@customer_service_account');
|
|
48
63
|
TUIChatEngine.chat.callExperimentalAPI('isFeatureEnabledForStat', Math.pow(2, 42));
|
|
49
64
|
})
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
.catch((error) => {
|
|
66
|
+
Toast({
|
|
67
|
+
message: TUITranslateService.t('TUIChat.登录失败'),
|
|
68
|
+
type: TOAST_TYPE.ERROR,
|
|
69
|
+
duration: 30000,
|
|
70
|
+
});
|
|
71
|
+
Log.l(error);
|
|
72
|
+
})
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
public
|
|
61
|
-
Log.l(`TUICustomerServer.init version:${version} SDKAppID:${SDKAppID} userID:${userID}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (TUICustomerServer.loggedInUserID === userID) {
|
|
75
|
+
public init(SDKAppID:number, userID:string, userSig:string) {
|
|
76
|
+
Log.l(`TUICustomerServer.init version:${version} SDKAppID:${SDKAppID} userID:${userID} isLoggedIn:${this.isLoggedIn} loggedInUserID:${this.loggedInUserID}`);
|
|
77
|
+
if (this.isLoggedIn) {
|
|
78
|
+
if (this.loggedInUserID === userID) {
|
|
65
79
|
return;
|
|
66
80
|
}
|
|
67
|
-
|
|
81
|
+
this.unInit().finally(() => {
|
|
82
|
+
this.isLoggedIn = false;
|
|
68
83
|
this.loginCustomerUIKit(SDKAppID, userID, userSig);
|
|
69
84
|
});
|
|
70
85
|
} else {
|
|
71
|
-
TUICustomerServer.isInitialized = true;
|
|
72
|
-
// Execute call server when native plugin TUICallKit exists
|
|
73
86
|
this.loginCustomerUIKit(SDKAppID, userID, userSig);
|
|
74
87
|
}
|
|
75
88
|
}
|
|
76
89
|
|
|
77
|
-
public
|
|
90
|
+
public initWithProfile(options: IInitWithProfile) {
|
|
91
|
+
const { SDKAppID, userID, userSig, nickName, avatar } = options;
|
|
92
|
+
Log.l(`TUICustomerServer.initWithProfile version:${version}`);
|
|
93
|
+
if (nickName) {
|
|
94
|
+
// chat 个人资料的昵称是 nick
|
|
95
|
+
this.myProfile.nick = nickName;
|
|
96
|
+
}
|
|
97
|
+
if (avatar) {
|
|
98
|
+
this.myProfile.avatar = avatar;
|
|
99
|
+
}
|
|
100
|
+
this.init(SDKAppID, userID, userSig);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public unInit() {
|
|
78
104
|
return TUIChatEngine.logout();
|
|
79
105
|
}
|
|
80
106
|
|
|
107
|
+
public sendTextMessage(options: SendMessageParams, sendMessageOptions?: SendMessageOptions) {
|
|
108
|
+
return TUIChatService.sendTextMessage(options, sendMessageOptions);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public sendCustomMessage(options: SendMessageParams, sendMessageOptions?: SendMessageOptions) {
|
|
112
|
+
return TUIChatService.sendCustomMessage(options, sendMessageOptions);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
public changeLanguage(language: string) {
|
|
116
|
+
return TUITranslateService.changeLanguage(language).then(() => {
|
|
117
|
+
Log.i(`language changed to ${language}`);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public getLoggedInUserID() {
|
|
122
|
+
return this.loggedInUserID;
|
|
123
|
+
}
|
|
124
|
+
|
|
81
125
|
// Determine if the current session is a customer service session
|
|
82
126
|
public isCustomerConversation(conversationID: string) {
|
|
83
127
|
const userID = (conversationID && conversationID.slice(3)) || '';
|
|
@@ -115,18 +159,31 @@ export default class TUICustomerServer {
|
|
|
115
159
|
Log.l(`TUICustomerServer.onCall method:${method} params:`, params);
|
|
116
160
|
if (method === TUIConstants.TUICustomerServicePlugin.SERVICE.METHOD.ACTIVE_CONVERSATION) {
|
|
117
161
|
if (this.isCustomerConversation(params.conversationID)) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
},
|
|
128
|
-
}, { onlineUserOnly: true});
|
|
162
|
+
// 如果有资料,确保资料更新完成(或失败)后再激活会话服务流
|
|
163
|
+
if (Object.keys(this.myProfile).length > 0) {
|
|
164
|
+
Log.l(`TUICustomerServer.onCall updateMyProfile:${JSON.stringify(this.myProfile)}`);
|
|
165
|
+
TUIUserService.updateMyProfile({...this.myProfile}).finally(() => {
|
|
166
|
+
this.activeServiceFlow(params);
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
this.activeServiceFlow(params);
|
|
170
|
+
}
|
|
129
171
|
}
|
|
130
172
|
}
|
|
131
173
|
}
|
|
174
|
+
|
|
175
|
+
// 激活会话服务流
|
|
176
|
+
private activeServiceFlow(params: any) {
|
|
177
|
+
TUIChatService.sendCustomMessage({
|
|
178
|
+
to: params.conversationID.slice(3),
|
|
179
|
+
conversationType: TUIChatEngine.TYPES.CONV_C2C,
|
|
180
|
+
payload: {
|
|
181
|
+
data: JSON.stringify({
|
|
182
|
+
src: '7',
|
|
183
|
+
customerServicePlugin: 0,
|
|
184
|
+
triggeredContent: typeof params.robotLang === 'undefined' ? undefined : { language: params.robotLang }
|
|
185
|
+
}),
|
|
186
|
+
},
|
|
187
|
+
}, { onlineUserOnly: true });
|
|
188
|
+
}
|
|
132
189
|
}
|