@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 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/server';
185
- import { CustomerServiceChat } from './ai-desk-customer-vue';
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/server';
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
- // 配置工具栏 button list,例如【转人工】
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/server';
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 { ref, onMounted } = vue;
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
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100027960326/b727aeef19cd11f0abe05254005ef0f7.png)
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-if="isH5" class="tool-icon" @click="toolShow">
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-btn-h5 {
209
- background-color: #1c66e5;
210
- color: #fff;
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 :payload="payload" />
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: #0365f9;
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: #0365f9;
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.3);
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: 80%;
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(() => parseMarkdown(streamContent.value));
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
- const _newChunksToAdd = chunks.value?.slice(prevChunksLength.value);
64
- startStreaming([_newChunksToAdd]);
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 {
@@ -118,7 +118,7 @@
118
118
  .message-marked {
119
119
  overflow: hidden;
120
120
  word-break: break-word;
121
- white-space: pre-wrap;
121
+ white-space: normal;
122
122
  display: flex;
123
123
  flex-direction: column;
124
124
  justify-content: flex-start;
@@ -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.renderCondition()" :key="index"
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
@@ -13,4 +13,6 @@ export {
13
13
  isMessageInvisible,
14
14
  CustomerServiceChat,
15
15
  isCustomerConversation
16
- };
16
+ };
17
+
18
+ export default TUICustomerServer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencentcloud/ai-desk-customer-vue",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Vue UIKit for AI Desk",
5
5
  "main": "index",
6
6
  "keywords": [
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
- static isInitialized: boolean;
30
+ private isLoggedIn: boolean;
20
31
  static instance: TUICustomerServer;
21
32
  private customerServiceAccounts: any[];
22
- static loggedInUserID: string;
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
- Log.i('TUICustomerServer.init ok');
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
- static loginCustomerUIKit(SDKAppID:number, userID:string, userSig:string) {
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
- TUICustomerServer.loggedInUserID = userID;
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
- .catch((error) => {
51
- Toast({
52
- message: TUITranslateService.t('TUIChat.登录失败'),
53
- type: TOAST_TYPE.ERROR,
54
- duration: 30000,
55
- });
56
- Log.l(error);
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 static init(SDKAppID:number, userID:string, userSig:string) {
61
- Log.l(`TUICustomerServer.init version:${version} SDKAppID:${SDKAppID} userID:${userID} isInitialized:${TUICustomerServer.isInitialized} loggedInUserID:${TUICustomerServer.loggedInUserID}`);
62
- // Backward compatibility, the new version executes the init operation by default in index.ts
63
- if (TUICustomerServer.isInitialized) {
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
- TUICustomerServer.unInit().finally(() => {
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 static async unInit() {
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
- TUIChatService.sendCustomMessage({
119
- to: params.conversationID.slice(3),
120
- conversationType: TUIChatEngine.TYPES.CONV_C2C,
121
- payload: {
122
- data: JSON.stringify({
123
- src: '7',
124
- customerServicePlugin: 0,
125
- triggeredContent: typeof params.robotLang === 'undefined' ? undefined : { language: params.robotLang }
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
  }