@tencentcloud/ai-desk-customer-vue 1.6.4 → 1.6.7

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 (23) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/assets/tool_more.svg +5 -0
  3. package/components/CustomerServiceChat/customer-queue-page/index.vue +7 -16
  4. package/components/CustomerServiceChat/index-web.vue +16 -3
  5. package/components/CustomerServiceChat/message-input/message-input-editor-web.vue +1 -1
  6. package/components/CustomerServiceChat/message-input-toolbar/rating-tool/index.vue +1 -1
  7. package/components/CustomerServiceChat/message-list/index-web.vue +12 -2
  8. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-robot-welcome.vue +9 -4
  9. package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-stream.vue +3 -0
  10. package/components/CustomerServiceChat/message-list/message-elements/message-thinking.vue +1 -1
  11. package/components/CustomerServiceChat/message-list/message-elements/simple-message-list/index.vue +0 -1
  12. package/components/CustomerServiceChat/message-toolbar-button/index.vue +285 -20
  13. package/components/CustomerServiceChat/message-toolbar-button/toolbar-button-end-human-service.vue +6 -1
  14. package/components/CustomerServiceChat/message-toolbar-button/toolbar-button-human-service.vue +5 -0
  15. package/components/CustomerServiceChat/message-toolbar-button/toolbar-button-service-rating.vue +6 -1
  16. package/components/common/Avatar/index.vue +2 -2
  17. package/constant.ts +0 -2
  18. package/locales/en/aidesk.ts +1 -1
  19. package/package.json +1 -3
  20. package/server.ts +14 -9
  21. package/utils/utils.ts +42 -0
  22. package/assets/video-play.png +0 -0
  23. package/components/CustomerServiceChat/message-list/message-elements/video-play.vue +0 -59
package/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## 1.6.7 @2025.12.30
2
+
3
+ ### Features
4
+ - initWithProfile 接口 Options 新增参数 clientCustomData,支持用户登录时传入自定义字段。
5
+
6
+ ### Fixed
7
+ - 优化关闭排队页面的用户体验。
8
+ - H5 端已知问题。
9
+
10
+ ## 1.6.6 @2025.12.22
11
+
12
+ ### Features
13
+ - 新增参数 showAllRobotWelcomeItems,支持一页展示欢迎卡片全部内容
14
+ - 工具栏快捷按钮支持设置【结束排队】。
15
+
16
+ ### Fixed
17
+ - 底部快捷订单的自定义数据类型不是 string 导致的未被机器人正确识别的问题。
18
+ - 用户昵称和头像可能被覆盖的问题。
19
+ - 工具栏快捷按钮超出屏幕时折叠超出按钮
20
+
1
21
  ## 1.6.4 @2025.11.25
2
22
 
3
23
  ### Features
@@ -0,0 +1,5 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M3 9C2.44775 9 2 8.55228 2 8C2 7.44772 2.44775 7 3 7C3.55225 7 4 7.44772 4 8C4 8.55228 3.55225 9 3 9Z" fill="black" style="fill:black;fill-opacity:1;"/>
3
+ <path d="M7 8C7 8.55228 7.44775 9 8 9C8.55225 9 9 8.55228 9 8C9 7.44772 8.55225 7 8 7C7.44775 7 7 7.44772 7 8Z" fill="black" style="fill:black;fill-opacity:1;"/>
4
+ <path d="M12 8C12 8.55228 12.4478 9 13 9C13.5522 9 14 8.55228 14 8C14 7.44772 13.5522 7 13 7C12.4478 7 12 7.44772 12 8Z" fill="black" style="fill:black;fill-opacity:1;"/>
5
+ </svg>
@@ -15,7 +15,7 @@
15
15
  </div>
16
16
  </div>
17
17
  <div class="customer-queue-end-button">
18
- <div :class="['customer-queue-end-button-content', isClicked ? 'button-clicked' : '']" @click="endQueuing">
18
+ <div :class="['customer-queue-end-button-content', isClicked ? 'button-clicked' : '']" @click="leaveQueue">
19
19
  {{ TUITranslateService.t("AIDesk.结束排队") }}
20
20
  </div>
21
21
  </div>
@@ -24,9 +24,8 @@
24
24
  <script lang="ts" setup>
25
25
  import vue from '../../../adapter-vue';
26
26
  const { ref, onMounted, onUnmounted } = vue;
27
- import { IConversationModel, StoreName, TUIChatService, TUIStore, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
28
- import { CUSTOM_MESSAGE_SRC } from '../../../constant';
29
- import { getTo, isEnabledMessageReadReceiptGlobal } from '../../../utils/utils';
27
+ import { IConversationModel, StoreName, TUIStore, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
28
+ import { getTo, leaveQueuing } from '../../../utils/utils';
30
29
  const currentConversation = ref<IConversationModel>();
31
30
  const isClicked = ref(false);
32
31
  const props = defineProps({
@@ -35,6 +34,7 @@ const props = defineProps({
35
34
  default: 0
36
35
  },
37
36
  });
37
+ const emits = defineEmits(['closeQueuePage']);
38
38
  onMounted(() => {
39
39
  TUIStore.watch(StoreName.CONV, {
40
40
  currentConversation: onCurrentConversationUpdate,
@@ -51,22 +51,13 @@ const onCurrentConversationUpdate = (conversation: IConversationModel) => {
51
51
  currentConversation.value = conversation;
52
52
  };
53
53
 
54
- const endQueuing = () => {
54
+ const leaveQueue = () => {
55
55
  if (isClicked.value) {
56
56
  return;
57
57
  }
58
58
  isClicked.value = true;
59
- TUIChatService.sendCustomMessage({
60
- to: getTo(currentConversation.value),
61
- conversationType: currentConversation.value.type,
62
- payload: {
63
- data: JSON.stringify({
64
- customerServicePlugin: 0,
65
- src: CUSTOM_MESSAGE_SRC.USER_END_CONVERSATION,
66
- }),
67
- },
68
- needReadReceipt: isEnabledMessageReadReceiptGlobal(),
69
- },{ onlineUserOnly:true });
59
+ leaveQueuing(getTo(currentConversation.value));
60
+ emits('closeQueuePage');
70
61
  }
71
62
  </script>
72
63
  <style lang="scss">
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div :class="['tui-chat', !isPC && 'tui-chat-h5']" :key="currentLanguage">
3
3
  <div
4
- v-if="currentConversationID && (!props.showQueuePage || queueNumber < 0 || queueNumber === undefined)"
4
+ v-if="currentConversationID && (!props.showQueuePage || queueNumber < 0 || queueNumber === undefined || hasLeftQueue)"
5
5
  :class="['tui-chat', !isPC && 'tui-chat-h5']"
6
6
  >
7
7
  <ChatHeader
@@ -94,8 +94,8 @@
94
94
  @close="() => { showFeedbackModal = false }"
95
95
  />
96
96
  </div>
97
- <div v-if="props.showQueuePage && queueNumber >= 0" :class="['tui-chat', !isPC && 'tui-chat-h5']">
98
- <CustomerQueuePage :queueNumber="queueNumber"/>
97
+ <div v-if="props.showQueuePage && queueNumber >= 0 && !hasLeftQueue" :class="['tui-chat', !isPC && 'tui-chat-h5']">
98
+ <CustomerQueuePage :queueNumber="queueNumber" @closeQueuePage="closeQueuePage"/>
99
99
  </div>
100
100
  </div>
101
101
  </template>
@@ -157,6 +157,7 @@ interface IProps {
157
157
  headerConfig?: IHeaderConfig;
158
158
  enableSendingAudio?: number;
159
159
  showQueuePage?: number;
160
+ showAllRobotWelcomeItems?: number;
160
161
  }
161
162
 
162
163
  const emits = defineEmits(['closeChat']);
@@ -177,6 +178,7 @@ const languageForShowList = ref<Array<string>>([]);
177
178
  const feedbackModalRef = ref();
178
179
  const showFeedbackModal = ref(false);
179
180
  const queueNumber = ref(-1);
181
+ const hasLeftQueue = ref(false);
180
182
  let timezone = '';
181
183
  let countryID = '';
182
184
  const props = withDefaults(defineProps<IProps>(), {
@@ -204,6 +206,7 @@ const props = withDefaults(defineProps<IProps>(), {
204
206
  enableURLDetection: 0,
205
207
  enableSendingAudio: 0,
206
208
  showQueuePage: 0,
209
+ showAllRobotWelcomeItems: 0,
207
210
  });
208
211
 
209
212
  const loginCustomerUIKit = () => {
@@ -320,6 +323,10 @@ const setEnableURLDetection = () => {
320
323
  state.set('enableURLDetection', props.enableURLDetection);
321
324
  }
322
325
 
326
+ const setShowAllRobotWelcomeItems = () => {
327
+ state.set('showAllRobotWelcomeItems', props.showAllRobotWelcomeItems);
328
+ }
329
+
323
330
  try {
324
331
  const userContext = TUILogin.getContext();
325
332
  if (userContext.userID == '' && props.SDKAppID !==0 && props.userID !=='' && props.userSig !==''){
@@ -333,6 +340,7 @@ try {
333
340
  setShowReadStatus();
334
341
  setShowTyping();
335
342
  setEnableURLDetection();
343
+ setShowAllRobotWelcomeItems();
336
344
  getTimeZoneAndCountry();
337
345
  if (isNonEmptyObject(props.bottomQuickOrder)) {
338
346
  showBottomQuickOrder.value = true;
@@ -521,8 +529,13 @@ function onDislike(messageInfo: Object) {
521
529
  function onIsQueuingUpdate(data: {conversationID: string, value: number}) {
522
530
  if (data && data.conversationID === currentConversationID.value) {
523
531
  queueNumber.value = data.value;
532
+ hasLeftQueue.value = false;
524
533
  }
525
534
  }
535
+
536
+ function closeQueuePage() {
537
+ hasLeftQueue.value = true;
538
+ }
526
539
  </script>
527
540
 
528
541
  <style scoped lang="scss" src="./style/index.scss">
@@ -361,7 +361,7 @@ function handlePasteText(e: ClipboardEvent) {
361
361
  const text = e.clipboardData?.getData('text/plain') || '';
362
362
  // if paste html in pc, paste by tiptap editor default
363
363
  // if paste text in pc or mobile, parse text to html to render emoji
364
- if (!html) {
364
+ if (isH5 || !html) {
365
365
  const renderArray = parseTextToRenderArray(text);
366
366
  insertEditorContent(renderArray);
367
367
  }
@@ -24,7 +24,7 @@ import ratingToolIcon from '../../../../assets/rating_tool_icon.svg'
24
24
  import ratingToolIconH5 from '../../../../assets/rating_tool_icon_h5.svg';
25
25
  import { getTo } from '../../../../utils/utils';
26
26
  import { CUSTOM_MESSAGE_SRC } from '../../../../constant';
27
- const { ref, onMounted, onUnmounted } = vue;
27
+ const { ref } = vue;
28
28
 
29
29
  const props = defineProps({
30
30
  isH5ToolShow:{
@@ -208,7 +208,6 @@ import {
208
208
  getBoundingClientRect,
209
209
  getScrollInfo,
210
210
  } from '@tencentcloud/universal-api';
211
- import { throttle } from 'lodash';
212
211
  import MessageText from './message-elements/message-text.vue';
213
212
  import MessageImage from './message-elements/message-image-web.vue';
214
213
  import MessageAudio from './message-elements/message-audio-web.vue';
@@ -237,6 +236,7 @@ import {
237
236
  deepCopy,
238
237
  isNonEmptyObject,
239
238
  updateCustomStore,
239
+ throttle
240
240
  } from '../../../utils/utils';
241
241
  import { isMessageInvisible, isThinkingMessage, isThinkingMessageOverTime, JSONToObject, isTransferMessageWithoutDesc } from '../../../utils/index';
242
242
  import { isCustomerConversation } from '../../../index';
@@ -820,13 +820,23 @@ function closeBottomQuickOrder() {
820
820
  }
821
821
 
822
822
  function sendBottomQuickOrder() {
823
+ if (!props.bottomQuickOrder) {
824
+ return;
825
+ }
826
+ const normalizedQuickOrder: QuickOrderModel = {
827
+ ...props.bottomQuickOrder,
828
+ customField: (props.bottomQuickOrder.customField || []).map(f => ({
829
+ name: f.name ? String(f.name) : '',
830
+ value: f.value ? String(f.value) : '',
831
+ })),
832
+ }
823
833
  TUIChatService.sendCustomMessage({
824
834
  to: currentConversationID.value.replace('C2C', ''),
825
835
  conversationType: 'C2C',
826
836
  payload: {
827
837
  data: JSON.stringify({
828
838
  src: CUSTOM_MESSAGE_SRC.PRODUCT_CARD,
829
- content: props.bottomQuickOrder,
839
+ content: normalizedQuickOrder,
830
840
  customerServicePlugin: 0,
831
841
  }),
832
842
  },
@@ -12,6 +12,7 @@
12
12
  </div>
13
13
  <div
14
14
  class="change-wrapper"
15
+ v-if="list.length > 3 && !showAllRobotWelcomeItems"
15
16
  @click="changeBranchList()"
16
17
  >
17
18
  <Icon :src="iconRefresh" />
@@ -44,6 +45,7 @@ import Icon from './customer-icon.vue';
44
45
  import iconQuestion from '../../../../../../assets/icon_question.png';
45
46
  import iconRefresh from '../../../../../../assets/icon_refresh.png';
46
47
  import { customerServicePayloadType } from '../../../../../../interface';
48
+ import state from '../../../../../../utils/state.js';
47
49
  const { reactive, toRefs, computed } = vue;
48
50
 
49
51
  interface Props {
@@ -68,13 +70,15 @@ export default {
68
70
  },
69
71
  emits: ['sendMessage'],
70
72
  setup(props: Props, { emit }) {
73
+ const showAllRobotWelcomeItems = state.get('showAllRobotWelcomeItems');
74
+ const welcomeItemList = props.payload.content.items || [];
71
75
  const data = reactive({
72
76
  // title
73
- title: props.payload?.content?.title || '',
77
+ title: props.payload.content.title || '',
74
78
  // all branch list
75
- list: props.payload?.content?.items || [],
79
+ list: welcomeItemList,
76
80
  // current branch list
77
- showList: (props.payload?.content?.items || []).slice(0, 3),
81
+ showList: showAllRobotWelcomeItems ? welcomeItemList : welcomeItemList.slice(0, 3),
78
82
  // current page number
79
83
  pageNumber: 1,
80
84
  });
@@ -116,7 +120,8 @@ export default {
116
120
  changeBranchList,
117
121
  iconQuestion,
118
122
  iconRefresh,
119
- longestContent
123
+ longestContent,
124
+ showAllRobotWelcomeItems,
120
125
  };
121
126
  },
122
127
  };
@@ -85,6 +85,9 @@ watch(() => props.payload, (newValue: string, oldValue: string) => {
85
85
  }
86
86
 
87
87
  const _payloadObject = JSONToObject(props.payload);
88
+ if (!_payloadObject.chunks) {
89
+ return;
90
+ }
88
91
  chunks.value = Array.isArray(_payloadObject.chunks) ? _payloadObject.chunks.join('') : _payloadObject.chunks;
89
92
  isFinished.value = _payloadObject.isFinished === 1;
90
93
 
@@ -11,7 +11,7 @@
11
11
  import vue from '../../../../adapter-vue';
12
12
  import Icon from '../../../common/Icon.vue';
13
13
  import loading_message from '../../../../assets/loading_message.svg';
14
- const { ref, watchEffect,onMounted,onUnmounted} = vue;
14
+ const { ref, onMounted, onUnmounted} = vue;
15
15
  export default {
16
16
  components:{
17
17
  Icon
@@ -158,7 +158,6 @@ import TUIChatEngine, {
158
158
  TUITranslateService,
159
159
  } from '@tencentcloud/chat-uikit-engine';
160
160
  import addIcon from '../../../../../assets/back.svg';
161
- import playIcon from '../../../../../assets/video-play.png';
162
161
  import Icon from '../../../../common/Icon.vue';
163
162
  import MessageContainer from './message-container.vue';
164
163
  import MessageRecord from '../message-record/index.vue';
@@ -1,22 +1,39 @@
1
1
  <template>
2
- <div class="toolbar-button-container">
3
- <template v-for="(item, index) in props.toolbarButtonList">
4
- <ToolbarButtonHumanService v-if="item.presetId === TOOLBAR_BUTTON_TYPE.HUMAN_SERVICE && shouldRender(item) && !isInHumanService" :title="item.title" :icon="item.icon"/>
5
- <ToolbarButtonServiceRating v-else-if="item.presetId === TOOLBAR_BUTTON_TYPE.SERVICE_RATING && shouldRender(item) && isInHumanService" :title="item.title" :icon="item.icon"/>
6
- <ToolbarButtonEndHumanService v-else-if="item.presetId === TOOLBAR_BUTTON_TYPE.END_HUMAN_SERVICE && shouldRender(item) && ((item.displayFlag === 1 && canEndConversation) || isInHumanService)" :title="item.title" :icon="item.icon"/>
7
- <div v-else-if="shouldRender(item) && !item.presetId" :key="index"
8
- :class="['toolbar-button', isH5 ? 'toolbar-button-h5' : '']" @click="onClick(item, index)">
9
- <Icon v-if="item.icon" class="toolbar-button-icon" :file="item.icon" width="18px" height="18px"/>
10
- <div class="toolbar-button-text">
2
+ <div class="toolbar-button-container" ref="toolbarContainerRef">
3
+ <template v-for="(item, index) in props.toolbarButtonList" :key="index">
4
+ <div :class="['toolbar-item-wrapper', index >= visibleCount ? 'measure-offscreen' : '']" :data-index="index" ref="toolbarWrappersRef">
5
+ <ToolbarButtonHumanService v-if="item.presetId === TOOLBAR_BUTTON_TYPE.HUMAN_SERVICE && shouldRender(item) && !isInHumanService" :title="item.title" :icon="item.icon"/>
6
+ <ToolbarButtonServiceRating v-else-if="item.presetId === TOOLBAR_BUTTON_TYPE.SERVICE_RATING && shouldRender(item) && isInHumanService" :title="item.title" :icon="item.icon"/>
7
+ <ToolbarButtonEndHumanService v-else-if="item.presetId === TOOLBAR_BUTTON_TYPE.END_HUMAN_SERVICE && shouldRender(item) && ((item.displayFlag === 1 && canEndConversation) || isInHumanService)" :title="item.title" :icon="item.icon"/>
8
+ <div v-else-if="shouldRender(item) && !item.presetId" :key="index"
9
+ :class="['toolbar-button', isH5 ? 'toolbar-button-h5' : '']" @click="onClick(item, index)">
10
+ <Icon v-if="item.icon" class="toolbar-button-icon" :file="item.icon" width="14px" height="14x"/>
11
+ <div class="toolbar-button-text">
11
12
  {{ item.title }}
13
+ </div>
12
14
  </div>
13
15
  </div>
14
16
  </template>
17
+ <div v-if="hiddenItems.length > 0" class="toolbar-button overflow-button" ref="toolbarOverflowRef" @click="togglePopover">
18
+ <Icon :file="ToolMoreIcon" width="14px" height="14px"/>
19
+ </div>
20
+ <div :class="['overflow-popover', showPopover ? 'show-popover' : '' ]" ref="toolbarPopoverRef">
21
+ <div v-for="(item, index) in hiddenItems" :key="item.id || ('hidden-' + index)">
22
+ <ToolbarButtonHumanService class="overflow-popover-item" v-if="item.presetId === TOOLBAR_BUTTON_TYPE.HUMAN_SERVICE && shouldRender(item) && !isInHumanService" :title="item.title" :icon="item.icon" @closePopover="closePopover"/>
23
+ <ToolbarButtonServiceRating class="overflow-popover-item" v-else-if="item.presetId === TOOLBAR_BUTTON_TYPE.SERVICE_RATING && shouldRender(item) && isInHumanService" :title="item.title" :icon="item.icon" @closePopover="closePopover"/>
24
+ <ToolbarButtonEndHumanService class="overflow-popover-item" v-else-if="item.presetId === TOOLBAR_BUTTON_TYPE.END_HUMAN_SERVICE && shouldRender(item) && ((item.displayFlag === 1 && canEndConversation) || isInHumanService)" :title="item.title" :icon="item.icon" @closePopover="closePopover"/>
25
+ <div v-else-if="shouldRender(item)" class="overflow-popover-item" @click="onClick(item, visibleCount + index)">
26
+ <Icon v-if="item.icon" class="toolbar-button-icon" :file="item.icon" width="16px" height="16px"/>
27
+ <span class="toolbar-button-text" style="text-align: left;">{{ item.title }}</span>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ <div class="measure-offscreen" ref="measureOverflowRef">···</div>
15
32
  </div>
16
33
  </template>
17
34
  <script lang="ts" setup>
18
35
  import vue from '../../../adapter-vue';
19
- const { ref, onMounted, onUnmounted } = vue;
36
+ const { ref, onMounted, onUnmounted, computed } = vue;
20
37
  import {
21
38
  TUIChatService,
22
39
  TUIStore,
@@ -27,10 +44,12 @@ import { isH5 } from '../../../utils/env';
27
44
  import { ToolbarButtonModel } from '../../../interface';
28
45
  import Icon from '../../common/Icon.vue';
29
46
  import { TOOLBAR_BUTTON_TYPE } from '../../../constant';
30
- import { isEnabledMessageReadReceiptGlobal, openSafeUrl, getTo, isNonEmptyObject, transferToTaskFlow, transferToHuman, debounce } from '../../../utils/utils';
47
+ import { isEnabledMessageReadReceiptGlobal, openSafeUrl, getTo, isNonEmptyObject, transferToTaskFlow, transferToHuman, debounce, leaveQueuing } from '../../../utils/utils';
31
48
  import ToolbarButtonHumanService from './toolbar-button-human-service.vue';
32
49
  import ToolbarButtonServiceRating from './toolbar-button-service-rating.vue';
33
50
  import ToolbarButtonEndHumanService from './toolbar-button-end-human-service.vue';
51
+ import ToolMoreIcon from '../../../assets/tool_more.svg';
52
+ import Log from '../../../utils/logger';
34
53
  interface IProps {
35
54
  toolbarButtonList?: ToolbarButtonModel[] | undefined;
36
55
  }
@@ -40,6 +59,21 @@ const props = withDefaults(defineProps<IProps>(), {});
40
59
  const isInHumanService = ref(false);
41
60
  const currentConversation = ref<IConversationModel>();
42
61
  const canEndConversation = ref(false);
62
+ const queueNumber = ref(-1);
63
+ const visibleCount = ref(props.toolbarButtonList ? props.toolbarButtonList.length : 0);
64
+ const showPopover = ref(false);
65
+
66
+ const toolbarContainerRef = ref<HTMLElement | null>(null);
67
+ const toolbarWrappersRef = ref<HTMLElement[] | HTMLElement | null>(null);
68
+ const toolbarOverflowRef = ref<HTMLElement | null>(null);
69
+ const toolbarPopoverRef = ref<HTMLElement | null>(null);
70
+ const measureOverflowRef = ref<HTMLElement | null>(null);
71
+
72
+ const buttonWidthList = ref<number[]>([]);
73
+ const overflowButtonWidth = 38;
74
+
75
+ let resizeObserver: ResizeObserver | null = null;
76
+ let mutationObserver: MutationObserver | null = null;
43
77
 
44
78
  onMounted(() => {
45
79
  TUIStore.watch(StoreName.CONV, {
@@ -47,8 +81,32 @@ onMounted(() => {
47
81
  });
48
82
  TUIStore.watch(StoreName.CUSTOM, {
49
83
  isInHumanService: onInHumanServiceUpdate,
50
- canEndConversation: onCanEndConversationUpdate
84
+ canEndConversation: onCanEndConversationUpdate,
85
+ isQueuing: onIsQueuingUpdate,
51
86
  });
87
+ getHiddenList();
88
+
89
+ try {
90
+ if (typeof (window as any).ResizeObserver !== 'undefined') {
91
+ resizeObserver = new (window as any).ResizeObserver(() => {
92
+ getHiddenList();
93
+ showPopover.value = false;
94
+ });
95
+ if (resizeObserver && toolbarContainerRef.value) {
96
+ resizeObserver.observe(toolbarContainerRef.value);
97
+ }
98
+ } else {
99
+ window.addEventListener('resize', getHiddenList);
100
+ }
101
+ if (typeof MutationObserver !== 'undefined' && toolbarContainerRef.value) {
102
+ mutationObserver = new MutationObserver(() => getHiddenList());
103
+ mutationObserver.observe(toolbarContainerRef.value, { childList: true, subtree: true });
104
+ }
105
+ } catch (e) {
106
+ window.addEventListener('resize', getHiddenList);
107
+ }
108
+
109
+ document.addEventListener('click', closePopoverOutside);
52
110
  });
53
111
 
54
112
  onUnmounted(() => {
@@ -57,8 +115,27 @@ onUnmounted(() => {
57
115
  });
58
116
  TUIStore.unwatch(StoreName.CUSTOM, {
59
117
  isInHumanService: onInHumanServiceUpdate,
60
- canEndConversation: onCanEndConversationUpdate
118
+ canEndConversation: onCanEndConversationUpdate,
119
+ isQueuing: onIsQueuingUpdate,
61
120
  });
121
+ if (resizeObserver) {
122
+ try {
123
+ resizeObserver.disconnect();
124
+ } catch {
125
+ Log.w('ResizeObserver disconnect failed');
126
+ }
127
+ resizeObserver = null;
128
+ }
129
+ if (mutationObserver) {
130
+ try {
131
+ mutationObserver.disconnect();
132
+ } catch {
133
+ Log.w('MutationObserver disconnect failed');
134
+ }
135
+ mutationObserver = null;
136
+ }
137
+ window.removeEventListener('resize', getHiddenList);
138
+ document.removeEventListener('click', closePopoverOutside);
62
139
  });
63
140
 
64
141
  const onCurrentConversationUpdate = (conversation: IConversationModel) => {
@@ -68,15 +145,31 @@ const onCurrentConversationUpdate = (conversation: IConversationModel) => {
68
145
  const onInHumanServiceUpdate = (data: {conversationID: string, value: boolean}) => {
69
146
  if (data && data.conversationID === currentConversation.value.conversationID) {
70
147
  isInHumanService.value = data.value;
148
+ if (!mutationObserver) {
149
+ getHiddenList();
150
+ }
71
151
  }
72
152
  };
73
153
 
74
154
  const onCanEndConversationUpdate = (data: {conversationID: string, value: boolean}) => {
75
155
  if (data && data.conversationID === currentConversation.value.conversationID) {
76
156
  canEndConversation.value = data.value;
157
+ if (!mutationObserver) {
158
+ getHiddenList();
159
+ }
160
+ }
161
+ }
162
+
163
+ const onIsQueuingUpdate = (data: {conversationID: string, value: number}) => {
164
+ if (data && data.conversationID === currentConversation.value.conversationID) {
165
+ queueNumber.value = data.value;
77
166
  }
78
167
  }
79
168
 
169
+ const closePopover = () => {
170
+ showPopover.value = false;
171
+ }
172
+
80
173
  const onClick = debounce((item:ToolbarButtonModel, index: number) => {
81
174
  if (!currentConversation.value) {
82
175
  return;
@@ -96,13 +189,23 @@ const onClick = debounce((item:ToolbarButtonModel, index: number) => {
96
189
  transferToTaskFlow(getTo(currentConversation.value), item.content.taskFlowID, item.content.description);
97
190
  } else if (item.type === 4 && isNonEmptyObject(item.content)) {
98
191
  transferToHuman(getTo(currentConversation.value), item.content.groupID, item.content.specificMemberList, item.content.description);
192
+ } else if (item.type === 5 && queueNumber.value >= 0) {
193
+ leaveQueuing(getTo(currentConversation.value));
99
194
  } else if (props.toolbarButtonList !== undefined && typeof props.toolbarButtonList[index].clickEvent === 'function') {
100
195
  props.toolbarButtonList[index].clickEvent();
101
196
  }
197
+ showPopover.value = false;
102
198
  }, 300);
103
199
 
104
200
  const shouldRender = (item: ToolbarButtonModel) => {
105
201
  if (item.isEnabled === 1) {
202
+ if (item.type === 5) {
203
+ // 转人工排队人数<0则不展示
204
+ if (queueNumber.value < 0) {
205
+ return false;
206
+ }
207
+ return true;
208
+ }
106
209
  return true;
107
210
  } else if (item.isEnabled === 0) {
108
211
  return false;
@@ -112,6 +215,113 @@ const shouldRender = (item: ToolbarButtonModel) => {
112
215
  return false;
113
216
  }
114
217
 
218
+ const togglePopover = () => {
219
+ showPopover.value = !showPopover.value;
220
+ };
221
+
222
+ const hiddenItems = computed(() => {
223
+ if (props.toolbarButtonList) {
224
+ return props.toolbarButtonList.slice(visibleCount.value) || [];
225
+ }
226
+ return [];
227
+ })
228
+
229
+ const getVisibleCount = () => {
230
+ const list = props.toolbarButtonList || [];
231
+ const buttonCount = list.length;
232
+ const container = toolbarContainerRef.value;
233
+ if (!container) {
234
+ visibleCount.value = buttonCount;
235
+ return;
236
+ }
237
+ const requiredRightGap = 11;
238
+ const available = container.clientWidth - requiredRightGap;
239
+ let sum = 0;
240
+ let visible = 0;
241
+ for (let i = 0; i < buttonCount; i++) {
242
+ const w = buttonWidthList.value[i] || 0;
243
+ if (i < buttonCount - 1) {
244
+ if (sum + w + overflowButtonWidth <= available) {
245
+ sum += w;
246
+ visible++;
247
+ } else {
248
+ break;
249
+ }
250
+ } else {
251
+ if (sum + w <= available) {
252
+ sum += w;
253
+ visible++;
254
+ } else {
255
+ break;
256
+ }
257
+ }
258
+ }
259
+ if (visible === 0 && buttonCount > 0) {
260
+ visibleCount.value = 0;
261
+ } else {
262
+ visibleCount.value = visible;
263
+ }
264
+ updatePopoverPosition();
265
+ };
266
+
267
+ const getHiddenList = debounce(() => {
268
+ const container = toolbarContainerRef.value;
269
+ if (!container) {
270
+ return;
271
+ }
272
+
273
+ const wrappersRaw = toolbarWrappersRef.value;
274
+ const buttonList = Array.isArray(wrappersRaw) ? wrappersRaw : (wrappersRaw ? [wrappersRaw] : []);
275
+ const list = props.toolbarButtonList || [];
276
+ const newWidths: number[] = [];
277
+
278
+ for (let i = 0; i < list.length; i++) {
279
+ const el = buttonList[i] as HTMLElement | undefined;
280
+ if (el && typeof el.getBoundingClientRect === 'function') {
281
+ const rect = el.getBoundingClientRect();
282
+ const style = window.getComputedStyle(el);
283
+ const ml = parseFloat(style.marginLeft || '0');
284
+ const mr = parseFloat(style.marginRight || '0');
285
+ newWidths[i] = rect.width + ml + mr;
286
+ } else {
287
+ newWidths[i] = 0;
288
+ }
289
+ }
290
+ buttonWidthList.value = newWidths;
291
+ getVisibleCount();
292
+ }, 100);
293
+
294
+ const closePopoverOutside = (e: MouseEvent) => {
295
+ const pop = toolbarPopoverRef.value;
296
+ const btn = toolbarOverflowRef.value;
297
+ if (!pop) {
298
+ return;
299
+ }
300
+ const target = e.target as Node | null;
301
+ if (!target) {
302
+ return;
303
+ }
304
+ if (pop.contains(target) || (btn && btn.contains(target))) {
305
+ return;
306
+ }
307
+ showPopover.value = false;
308
+ };
309
+
310
+ const updatePopoverPosition = () => {
311
+ const pop = toolbarPopoverRef.value;
312
+ const btn = toolbarOverflowRef.value;
313
+ if (!btn) {
314
+ return;
315
+ }
316
+ const btnRect = btn.getBoundingClientRect();
317
+ if (pop) {
318
+ const left = btnRect.right - pop.offsetWidth;
319
+ const top = btnRect.top - pop.offsetHeight - 6;
320
+ pop.style.left = `${left}px`;
321
+ pop.style.top = `${top}px`;
322
+ }
323
+ };
324
+
115
325
  </script>
116
326
  <style>
117
327
  .toolbar-button-container {
@@ -119,13 +329,8 @@ const shouldRender = (item: ToolbarButtonModel) => {
119
329
  flex-direction: row !important;
120
330
  margin: 5px !important;
121
331
  align-items: center ;
122
- overflow-x: auto; /* 允许横向滚动 */
123
- scrollbar-width: none; /* Firefox 隐藏滚动条 */
124
- -ms-overflow-style: none; /* IE/Edge 隐藏滚动条 */
332
+ overflow-x: hidden;
125
333
  min-height: 28px;
126
- &::-webkit-scrollbar {
127
- display: none; /* Chrome 隐藏滚动条 */
128
- }
129
334
  }
130
335
 
131
336
  .toolbar-button {
@@ -138,10 +343,11 @@ const shouldRender = (item: ToolbarButtonModel) => {
138
343
  margin-left: 10px;
139
344
  white-space: nowrap;
140
345
  user-select: none;
346
+ position: relative;
141
347
  }
142
348
 
143
349
  .toolbar-button:first-child {
144
- margin-left: 5px;
350
+ margin-left: 11px;
145
351
  }
146
352
 
147
353
  .toolbar-button-h5 {
@@ -161,4 +367,63 @@ const shouldRender = (item: ToolbarButtonModel) => {
161
367
  min-width: 25px;
162
368
  text-align: center;
163
369
  }
370
+ .overflow-button {
371
+ position: relative;
372
+ user-select: none;
373
+ background-color: #fff;
374
+ padding: 5px 14px;
375
+ }
376
+
377
+ .overflow-popover {
378
+ position: fixed;
379
+ z-index: 9999;
380
+ background: #fff;
381
+ border: 1px solid #f9fafc;
382
+ box-shadow: 0 1px 6px 0 rgba(0,0,0,0.06), 0 6px 16px 0 rgba(0,0,0,0.06);
383
+ border-radius: 8px;
384
+ padding: 6px;
385
+ min-width: 120px;
386
+ white-space: nowrap;
387
+ transform-origin: right bottom;
388
+ visibility: hidden;
389
+ max-height: 200px;
390
+ overflow-y: auto;
391
+ max-width: 100px;
392
+ min-width: 25px;
393
+ overflow-x: hidden;
394
+ scrollbar-width: none;
395
+ -ms-overflow-style: none;
396
+ &::-webkit-scrollbar {
397
+ display: none;
398
+ }
399
+ }
400
+ .show-popover {
401
+ display: block;
402
+ visibility: visible;
403
+ }
404
+
405
+ .overflow-popover-item {
406
+ display: flex;
407
+ align-items: center;
408
+ padding: 8px;
409
+ cursor: pointer;
410
+ white-space: nowrap;
411
+ font-size: 12px;
412
+ border-radius: 4px;
413
+ border: none;
414
+ margin: 0 !important;
415
+ box-shadow: none;
416
+ }
417
+ .overflow-popover-item:hover {
418
+ background: #f9fafc;
419
+ }
420
+
421
+ .measure-offscreen {
422
+ position: absolute;
423
+ visibility: hidden;
424
+ white-space: nowrap;
425
+ padding: 5px 14px;
426
+ border-radius: 20px;
427
+ border: 1px solid #E7EAEF;
428
+ }
164
429
  </style>
@@ -26,6 +26,8 @@ const props = withDefaults(defineProps<IProps>(), {
26
26
  icon: ''
27
27
  })
28
28
 
29
+ const emits = defineEmits(['closePopover']);
30
+
29
31
  onMounted(() => {
30
32
  TUIStore.watch(StoreName.CONV, {
31
33
  currentConversation: onCurrentConversationUpdate,
@@ -57,7 +59,10 @@ const onClick = () => {
57
59
  }),
58
60
  },
59
61
  needReadReceipt: isEnabledMessageReadReceiptGlobal(),
60
- },{ onlineUserOnly:true });
62
+ },{ onlineUserOnly:true })
63
+ .finally(() => {
64
+ emits('closePopover');
65
+ });
61
66
  }
62
67
 
63
68
  </script>
@@ -24,6 +24,8 @@ const props = withDefaults(defineProps<IProps>(), {
24
24
  icon: ''
25
25
  });
26
26
 
27
+ const emits = defineEmits(['closePopover']);
28
+
27
29
  onMounted(() => {
28
30
  TUIStore.watch(StoreName.CONV, {
29
31
  currentConversation: onCurrentConversationUpdate,
@@ -51,6 +53,9 @@ const onClick = debounce(() => {
51
53
  text: TUITranslateService.t('AIDesk.转人工服务')
52
54
  },
53
55
  needReadReceipt: isEnabledMessageReadReceiptGlobal(),
56
+ })
57
+ .finally(() => {
58
+ emits('closePopover');
54
59
  });
55
60
  }, 300);
56
61
 
@@ -25,6 +25,8 @@ const props = withDefaults(defineProps<IProps>(), {
25
25
  icon: ''
26
26
  })
27
27
 
28
+ const emits = defineEmits(['closePopover']);
29
+
28
30
  onMounted(() => {
29
31
  TUIStore.watch(StoreName.CONV, {
30
32
  currentConversation: onCurrentConversationUpdate,
@@ -55,7 +57,10 @@ const onClick = debounce(() => {
55
57
  }),
56
58
  },
57
59
  needReadReceipt: isEnabledMessageReadReceiptGlobal(),
58
- },{ onlineUserOnly: true });
60
+ },{ onlineUserOnly: true })
61
+ .finally(() => {
62
+ emits('closePopover');
63
+ });
59
64
  }, 300);
60
65
 
61
66
  </script>
@@ -41,12 +41,12 @@ interface IEmits {
41
41
  }
42
42
 
43
43
  const defaultAvatarUrl = ref(
44
- 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png',
44
+ 'https://web.sdk.qcloud.com/im/desk/assets/user_default_avatar.png',
45
45
  );
46
46
  const emits = defineEmits<IEmits>();
47
47
  const props = withDefaults(defineProps<IProps>(), {
48
48
  // uniapp vue2 does not support constants in defineProps
49
- url: 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png',
49
+ url: 'https://web.sdk.qcloud.com/im/desk/assets/user_default_avatar.png',
50
50
  size: '36px',
51
51
  borderRadius: '50%',
52
52
  useSkeletonAnimation: false,
package/constant.ts CHANGED
@@ -165,8 +165,6 @@ export const INPUT_TOOLBAR_TYPE = {
165
165
  AUDIO: 'audio',
166
166
  };
167
167
 
168
- export const USER_DEFAULT_AVATAR = 'https://web.sdk.qcloud.com/im/desk/assets/user_default_avatar.png';
169
-
170
168
  export enum ReadState {
171
169
  Read,
172
170
  Unread,
@@ -38,6 +38,6 @@ const AIDesk = {
38
38
  "排队等待中": "In Queue",
39
39
  "排队等待话术": "We apologize for the wait. All agents are currently assisting other customers. You will be connected shortly. Thank you for your patience.",
40
40
  "当前前方排队人数": "Queue Position",
41
- "结束排队": "Exit Queue"
41
+ "结束排队": "Leave Queue"
42
42
  }
43
43
  export default AIDesk;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencentcloud/ai-desk-customer-vue",
3
- "version": "1.6.4",
3
+ "version": "1.6.7",
4
4
  "description": "Vue2/Vue3 UIKit for AI Desk",
5
5
  "main": "index",
6
6
  "keywords": [
@@ -31,11 +31,9 @@
31
31
  "@tiptap/extension-text": "2.0.0-beta.220",
32
32
  "@tiptap/pm": "2.0.0-beta.220",
33
33
  "@tiptap/suggestion": "2.0.0-beta.220",
34
- "@types/lodash": "^4.14.202",
35
34
  "countries-and-timezones": "^3.8.0",
36
35
  "dayjs": "^1.11.10",
37
36
  "js-audio-recorder": "^1.0.7",
38
- "lodash": "^4.17.21",
39
37
  "marked": "^6.0.0",
40
38
  "mp-html": "^2.5.0",
41
39
  "vue-clipboard3": "2.0.0"
package/server.ts CHANGED
@@ -17,9 +17,8 @@ import TUIChatEngine, {
17
17
  import Log from './utils/logger';
18
18
  import { version } from './package.json'
19
19
  import { Toast, TOAST_TYPE } from "./components/common/Toast/index-web";
20
- import { switchReadStatus, transferToHuman, transferToTaskFlow, validateUserID, updateCustomStore, isNonEmptyObject } from "./utils/utils";
20
+ import { switchReadStatus, transferToHuman, transferToTaskFlow, validateUserID, isNonEmptyObject } from "./utils/utils";
21
21
  import state from "./utils/state";
22
- import { USER_DEFAULT_AVATAR } from "./constant";
23
22
  import { vueVersion } from "./adapter-vue-web";
24
23
  import { ITransferToHumanModel, ITransferToTaskFlowModel } from './interface';
25
24
 
@@ -30,6 +29,7 @@ interface IInitWithProfile {
30
29
  nickName?: string,
31
30
  avatar?: string,
32
31
  customerServiceID?: string,
32
+ clientCustomData?: string,
33
33
  }
34
34
 
35
35
  interface IProfile {
@@ -46,6 +46,7 @@ export default class TUICustomerServer {
46
46
  private myProfile: IProfile;
47
47
  private paramsForActiveAgain: any;
48
48
  private loginFailToasts: any[];
49
+ private clientCustomData: string;
49
50
  constructor() {
50
51
  TUICore.registerService(TUIConstants.TUICustomerServicePlugin.SERVICE.NAME, this);
51
52
  TUICore.registerExtension(TUIConstants.TUIContact.EXTENSION.CONTACT_LIST.EXT_ID, this);
@@ -54,7 +55,8 @@ export default class TUICustomerServer {
54
55
  this.loginFailToasts = [];
55
56
  this.isLoggedIn = false;
56
57
  this.loggedInUserID = '';
57
- this.myProfile = { avatar: USER_DEFAULT_AVATAR };
58
+ this.myProfile = {};
59
+ this.clientCustomData = '';
58
60
  }
59
61
 
60
62
  static getInstance(): TUICustomerServer {
@@ -139,19 +141,15 @@ export default class TUICustomerServer {
139
141
  }
140
142
 
141
143
  public async initWithProfile(options: IInitWithProfile) {
142
- const { SDKAppID, userID, userSig, nickName, avatar, customerServiceID } = options;
144
+ const { SDKAppID, userID, userSig, nickName, avatar, customerServiceID, clientCustomData } = options;
143
145
  Log.l(`TUICustomerServer.initWithProfile version:${version}`);
144
146
  if (nickName) {
145
147
  // chat 个人资料的昵称是 nick
146
148
  this.myProfile.nick = nickName;
147
- } else {
148
- this.myProfile.nick = '';
149
149
  }
150
150
 
151
151
  if (avatar) {
152
152
  this.myProfile.avatar = avatar;
153
- } else {
154
- this.myProfile.avatar = USER_DEFAULT_AVATAR;
155
153
  }
156
154
 
157
155
  if (customerServiceID) {
@@ -160,6 +158,12 @@ export default class TUICustomerServer {
160
158
  this.customerServiceIDList.push(this.currentCustomerServiceID);
161
159
  }
162
160
  }
161
+
162
+ if (clientCustomData) {
163
+ this.clientCustomData = clientCustomData;
164
+ } else {
165
+ this.clientCustomData = '';
166
+ }
163
167
  return this.init(SDKAppID, userID, userSig);
164
168
  }
165
169
 
@@ -281,7 +285,7 @@ export default class TUICustomerServer {
281
285
 
282
286
  // 激活会话服务流
283
287
  private activeServiceFlow(params: any) {
284
- Log.i(`TUICustomerServer.activeServiceFlow params: language:${params.robotLang} country:${params.country} timezone:${params.timezone}`);
288
+ Log.i(`TUICustomerServer.activeServiceFlow params: language:${params.robotLang} country:${params.country} timezone:${params.timezone} clientCustomData:${this.clientCustomData}`);
285
289
  this.paramsForActiveAgain = { ...params };
286
290
  TUIChatService.sendCustomMessage({
287
291
  to: params.conversationID.slice(3),
@@ -294,6 +298,7 @@ export default class TUICustomerServer {
294
298
  language: params.robotLang,
295
299
  country: params.country,
296
300
  timezone: params.timezone,
301
+ clientCustomData: this.clientCustomData,
297
302
  }
298
303
  }),
299
304
  },
package/utils/utils.ts CHANGED
@@ -376,4 +376,46 @@ export function debounce(func, delay) {
376
376
  func.apply(context, args);
377
377
  }, delay);
378
378
  }
379
+ }
380
+
381
+ export function leaveQueuing(conversationID: string) {
382
+ TUIChatService.sendCustomMessage({
383
+ to: conversationID,
384
+ conversationType: TUIChatEngine.TYPES.CONV_C2C,
385
+ payload: {
386
+ data: JSON.stringify({
387
+ customerServicePlugin: 0,
388
+ src: CUSTOM_MESSAGE_SRC.USER_END_CONVERSATION,
389
+ }),
390
+ },
391
+ needReadReceipt: isEnabledMessageReadReceiptGlobal(),
392
+ },{ onlineUserOnly: true });
393
+ };
394
+
395
+ export function throttle(
396
+ func,
397
+ wait = 200,
398
+ options?: { leading?: boolean;}
399
+ ) {
400
+ let last = 0;
401
+ let timeout;
402
+ const leading = options?.leading !== undefined ? options.leading : true;
403
+
404
+ return function (this: any, ...args: any[]) {
405
+ const now = Date.now();
406
+ if (!last && !leading) {
407
+ last = now;
408
+ }
409
+
410
+ const remaining = wait - (now - last);
411
+
412
+ if (remaining <= 0) {
413
+ if (timeout) {
414
+ clearTimeout(timeout);
415
+ timeout = null;
416
+ }
417
+ last = now;
418
+ func.apply(this, args);
419
+ }
420
+ };
379
421
  }
Binary file
@@ -1,59 +0,0 @@
1
- <template>
2
- <div class="dialog-video">
3
- <video
4
- v-if="isShow"
5
- id="videoEle"
6
- class="video-box"
7
- :src="videoData"
8
- controls
9
- autoplay
10
- />
11
- </div>
12
- </template>
13
-
14
- <script lang="ts" setup>
15
- import vue from '../../TencentCloudCustomer/tui-customer-service-plugin-test/adapter-vue';
16
- import { TUIGlobal } from '@tencentcloud/universal-api';
17
- import { onLoad, onReady } from '@dcloudio/uni-app';
18
- const { ref } = vue;
19
-
20
- const videoData = ref();
21
- const isShow = ref(false);
22
- const videoContext = ref();
23
- onLoad((option: any) => {
24
- const decodedUrl = decodeURIComponent(option?.videoUrl);
25
- videoData.value = decodedUrl;
26
- isShow.value = true;
27
- });
28
-
29
- onReady(() => {
30
- isShow.value = true;
31
- videoContext.value = TUIGlobal.createVideoContext('videoEle');
32
- });
33
- </script>
34
- <style lang="scss" scoped>
35
- .dialog-video {
36
- position: fixed;
37
- z-index: 999;
38
- width: 100vw;
39
- height: 100vh;
40
- background: rgba(#000, 0.6);
41
- top: 0;
42
- left: 0;
43
- right: 0;
44
- bottom: 0;
45
- display: flex;
46
- justify-content: center;
47
- align-items: center;
48
-
49
- .video-box {
50
- position: absolute;
51
- width: 100vw;
52
- height: 100vh;
53
- top: 0;
54
- left: 0;
55
- right: 0;
56
- bottom: 0;
57
- }
58
- }
59
- </style>