This commit is contained in:
pengxiaolong
2025-05-13 19:39:53 +08:00
parent 37da6765b8
commit c006a8e63d
1232 changed files with 96963 additions and 883 deletions

View File

@@ -0,0 +1,40 @@
{
"extends": ["stylelint-config-standard"],
"overrides": [
{
"files": ["*.scss", "**/*.scss"],
"extends": ["stylelint-config-standard-scss"]
},
{
"files": ["*.vue", "**/*.vue"],
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-standard-vue/scss"
]
}
],
"rules": {
"declaration-block-no-redundant-longhand-properties": [
true,
{
"ignoreShorthands": ["inset"]
}
],
"font-family-no-missing-generic-family-keyword": [
true,
{
"ignoreFontFamilies": ["PingFangSC-Regular", "PingFangSC-Medium"]
}
],
"unit-no-unknown": [
true,
{
"ignoreUnits": ["rpx"]
}
],
"color-function-notation": "legacy",
"property-no-vendor-prefix": null,
"scss/at-extend-no-missing-placeholder": null,
"alpha-value-notation": "number"
}
}

View File

@@ -0,0 +1,50 @@
## [2.0.9] (2024-03-29)
## [2.0.8] (2024-03-15)
### Fix
- 修复 vue2 Props 中明确变量类型报错的问题
## [2.0.7] (2024-03-15)
### Fix
- 修复 rating 消息发送的问题
## [2.0.6] (2024-03-01)
### Features
- 升级 @tencentcloud/universal-api
### Fix
- 修复 message-from 中 FormBranch 组件的传参
- 修复客服不展示消息占位的问题
- 修复 src=9 消息的不展示的问题
## [2.0.5] (2024-02-04)
### Fix
- 修复已知问题,提升稳定性
## [2.0.4] (2024-01-19)
### Fix
- 修复已知问题,提升稳定性
## [2.0.3] (2024-01-12)
### Fix
- 修复已知问题,提升稳定性
## [2.0.2] (2024-01-05)
### Fix
- 修复已知问题,提升稳定性
## [2.0.0] (2023-12-21)
### Features
- 支持全平台客服插件

View File

@@ -0,0 +1,36 @@
## 简介
tui-customer-service-plugin 是基于 uikit 的客服插件,专为企业客服人员设计,用于为客户提供咨询解答服务。它具备丰富的功能,如消息快速回复、自动回复、访客信息查看、营销数据分析、客户管理等,并且支持多渠道接入,可以实现多样化集成。 在电商行业中,客服插件的功能更加贴合电商行业的特点,例如购物车信息互通、链接自动识别、多店铺管理等功能。它具有以下优势:
- 提高客服效率:客服插件可以快速回复客户的问题,并且支持自动回复功能,可以大大减少客服人员的工作量;
- 提升客户体验:客服插件可以实时显示客户信息,方便客服人员了解客户需求,提供更加精准的服务,提升客户体验;
- 加强客户管理:客服插件可以记录客户的历史对话内容,方便了解客户的需求和反馈,提升客户管理水平;
- 优化营销策略:客服插件可以收集客户的意见和建议,帮助企业优化营销策略,提升营销效果。
### 前提条件
集成 @tencentcloud/chat-uikit-vue2 ≥ 0.1.6 / @tencentcloud/chat-uikit-vue3 ≥ 0.1.6 或@tencentcloud/chat-uikit-uniapp
≥ 0.1.6
> 如未集成,请务必先根据 [Vue2 版本 TUIKit 快速集成指引](https://cloud.tencent.com/document/product/269/68493)、[Vue3 版本 TUIKit 快速集成指引](https://cloud.tencent.com/document/product/269/68493) 或 [uniapp 版本 TUIKit 快速集成指引](https://cloud.tencent.com/document/product/269/64507) 进行集成。<br />
> 如已集成,请务必升级版本 ≥ 0.1.6.
## 快速集成
### 步骤1接入 chat-uikit-vue2 / chat-uikit-vue3 或 chat-uikit-uniapp
已集成 @tencentcloud/chat-uikit-vue2 ≥ 0.1.6 / @tencentcloud/chat-uikit-vue3 ≥ 0.1.6 / @tencentcloud/chat-uikit-uniapp ≥ 0.1.6 请忽略此步骤。
如未集成,首先请您跟随指引 [Vue2 版本 TUIKit 快速集成指引](https://cloud.tencent.com/document/product/269/68493)、[Vue3 版本 TUIKit 快速集成指引](https://cloud.tencent.com/document/product/269/68493) 或 [uniapp 版本 TUIKit 快速集成指引](https://cloud.tencent.com/document/product/269/64507)的集成。
TUIKit Demo 登录成功后,如果您已经开通了客服插件,可以直接体验 Demo 的客服号 “线上商城”和 “线上医疗问诊”。
如果您需要创建属于自己的客服号,可以参见后续步骤操作。
### 步骤2控制台开通客服插件
请单击 [插件市场 > 客服插件](https://console.cloud.tencent.com/im/plugin/DesKKit) 免费试用或购买插件。详情请参见:[插件市场 > 概述与开通指引](https://cloud.tencent.com/document/product/269/92648)。
>!注意:<br/>
每个插件限免费试用 1 次,有效期 7 天,试用结束后将停服,请提前购买。
## 自定义客服号
详见: [在线客服插件](https://cloud.tencent.com/document/product/269/102783)

View File

@@ -0,0 +1,11 @@
let vueVersion: number;
// #ifndef VUE3
export * from '@vue/composition-api';
vueVersion = 2;
// #endif
// #ifdef VUE3
export * from 'vue';
vueVersion = 3;
// #endif
export { vueVersion };

View File

@@ -0,0 +1,73 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as _Vue from 'vue';
import * as VueApi from '@vue/composition-api';
let VueBasic: any = {
default: {},
};
VueBasic = _Vue ? _Vue : VueBasic;
let vueVersion: number;
let createVNode = (
arg1: any,
arg2: any,
): { component: any; props: any; data: any } => {
return {} as { component: any; props: any; data: any };
};
let render = (arg1: any, arg2: any) => {
return;
};
let defineProps = () => {
return;
};
let defineEmits = () => {
return;
};
let withDefaults = (arg: any) => {
return arg;
};
try {
if (
(VueBasic as any)?.default?.version
&& (VueBasic as any)?.default?.version?.startsWith('2.7.')
) {
// >= Vue 2.7.0
vueVersion = 2.7;
} else if (
(VueBasic as any)?.default?.version
&& (VueBasic as any)?.default?.version?.startsWith('2.')
) {
// < Vue 2.7.0
vueVersion = 2;
} else {
// >= Vue 3.0.0
vueVersion = 3;
createVNode = (VueBasic as any)?.createVNode;
render = (VueBasic as any)?.render;
defineProps = (VueBasic as any)?.defineProps;
defineEmits = (VueBasic as any)?.defineEmits;
withDefaults = (VueBasic as any)?.withDefaults;
// exportedAPIOrigin = Vue;
}
} catch (error: any) {
// >= Vue 3.0.0
vueVersion = 3;
createVNode = (VueBasic as any)?.createVNode;
render = (VueBasic as any)?.render;
defineProps = (VueBasic as any)?.defineProps;
defineEmits = (VueBasic as any)?.defineEmits;
withDefaults = (VueBasic as any)?.withDefaults;
}
console.warn(`[adapter-vue]: vue version is ${vueVersion}`);
let vue: any = VueBasic;
if (vueVersion === 2) {
vue = VueApi;
}
export { vueVersion, render, createVNode, defineProps, defineEmits, withDefaults };
export { vue };

View File

@@ -0,0 +1,11 @@
import Vue from 'vue';
import * as VueUni from './adapter-vue-uniapp';
import * as VueWeb from './adapter-vue-web';
let vue: any = VueUni;
if (window && !(window as any).uni) {
vue = { ...VueWeb, ...(VueWeb as any).vue };
}
export default vue as typeof Vue;

View File

@@ -0,0 +1,3 @@
<svg width="5" height="9" viewBox="0 0 5 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Path 2" d="M3.62095 4.5L-0.000976562 8.30849L0.719462 9L4.99902 4.5L0.719462 0L-0.000976562 0.691513L3.62095 4.5Z" fill="#368DFF"/>
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4.02944 0 0 4.02944 0 9C0 13.9706 4.02944 18 9 18C11.1575 18 13.1378 17.2408 14.6881 15.975H17.6986L16.5239 13.9405C17.4571 12.5223 18 10.8246 18 9C18 4.02944 13.9706 0 9 0Z" fill="#09B657" style="fill:#09B657;fill:color(display-p3 0.0356 0.7125 0.3412);fill-opacity:1;"/>
<path d="M8.104 11.144C7.624 8.472 11.384 7.768 11.384 5.944C11.384 4.84 10.6 4.04 8.968 4.04C7.864 4.04 6.856 4.52 5.928 5.48L5.08 4.696C6.136 3.576 7.384 2.824 9.112 2.824C11.368 2.824 12.824 3.992 12.824 5.832C12.824 8.248 9 8.744 9.416 11.144H8.104ZM8.792 15.08C8.216 15.08 7.768 14.68 7.768 14.04C7.768 13.4 8.232 12.968 8.792 12.968C9.352 12.968 9.816 13.4 9.816 14.04C9.816 14.68 9.352 15.08 8.792 15.08Z" fill="white" style="fill:white;fill:white;fill-opacity:1;"/>
</svg>

After

Width:  |  Height:  |  Size: 904 B

View File

@@ -0,0 +1,4 @@
<svg width="12" height="17" viewBox="0 0 12 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.71 4.355L9.28 5.785C9.731 6.434 10 7.217 10 8.065C10 10.271 8.206 12.065 6 12.065V10L3 13.033L6 16.065V14.065C9.314 14.065 12 11.379 12 8.065C12 6.663 11.515 5.377 10.71 4.355Z" fill="#006EFF" style="fill:#006EFF;fill:color(display-p3 0.0000 0.4314 1.0000);fill-opacity:1;"/>
<path d="M6 4.065V6.065L9 3.033L6 0V2.065C2.686 2.065 0 4.751 0 8.065C0 9.467 0.485 10.753 1.29 11.775L2.72 10.345C2.269 9.697 2 8.913 2 8.065C2 5.859 3.794 4.065 6 4.065Z" fill="#006EFF" style="fill:#006EFF;fill:color(display-p3 0.0000 0.4314 1.0000);fill-opacity:1;"/>
</svg>

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

View File

@@ -0,0 +1,56 @@
<template>
<!--本地 icon 资源, uniapp 打包到 app 仅支持标签 image, 打包小程序和 H5 均可支持标签 img -->
<div class="common-icon-container">
<image
v-if="isApp"
class="common-icon"
:src="props.src"
:style="{ width: props.width, height: props.height }"
/>
<img
v-else
class="common-icon"
:src="props.src"
:style="{ width: props.width, height: props.height }"
>
</div>
</template>
<script lang="ts">
import { isApp } from '../utils/env';
interface Props {
src: string;
width?: string;
height?: string;
}
export default {
props: {
src: {
type: String,
default: '',
},
width: {
type: String,
default: '16px',
},
height: {
type: String,
default: '16px',
},
},
setup(props: Props) {
return {
props,
isApp,
};
},
};
</script>
<style lang="scss" scoped>
.common-icon-container {
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<div class="branch-card">
<p
v-if="content.header || content.title"
class="branch-title"
>
{{ content.header || content.title }}
</p>
<div
v-for="(item, index) in content.items"
:key="index"
class="branch-item"
:style="{ borderWidth: content.header ? '1px 0 0px 0' : '0px 0 1px 0' }"
@click="handleContentListItemClick(item)"
>
{{ item.content }}
<Icon :src="iconRight" />
</div>
</div>
</template>
<script lang="ts">
import vue from '../adapter-vue';
import { customerServicePayloadType } from '../interface';
import iconRight from '../assets/iconRight.svg';
import Icon from './customer-icon.vue';
const { computed } = vue;
interface Props {
payload: customerServicePayloadType;
}
interface branchItem {
content: string;
desc: string;
}
export default {
components: {
Icon,
},
props: {
payload: {
type: Object as () => customerServicePayloadType,
default: () => ({}),
},
},
emits: ['sendMessage'],
setup(props: Props, { emit }) {
const content = computed(() => {
return (
props?.payload?.content || {
header: undefined,
items: [],
}
);
});
const handleContentListItemClick = (branch: branchItem) => {
emit('sendMessage', { text: branch.content });
};
return {
content,
handleContentListItemClick,
iconRight,
};
},
};
</script>
<style lang="scss">
.branch-card {
min-width: 250px;
max-width: 350px;
.branch-title {
margin-bottom: 8px;
border-radius: 0 10px 10px;
}
.branch-item {
display: flex;
justify-content: space-between;
border-style: dotted;
border-color: #d8d8d8;
font-weight: 400;
color: rgba(54, 141, 255, 1);
padding-top: 5px;
cursor: pointer;
padding-bottom: 5px;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<div class="custom">
<div
v-if="
payload.src === CUSTOM_MESSAGE_SRC.BRANCH ||
payload.src === CUSTOM_MESSAGE_SRC.BRANCH_NUMBER ||
(payload.src === CUSTOM_MESSAGE_SRC.ROBOT_MSG &&
payload.subtype !== 'welcome_msg')
"
>
<MessageBranch
:payload="payload"
@sendMessage="sendTextMessage"
/>
</div>
<div
v-if="
payload.src === CUSTOM_MESSAGE_SRC.ROBOT_MSG &&
payload.subtype === 'welcome_msg'
"
>
<MessageIMRobotWelcome
:payload="payload"
@sendMessage="sendTextMessage"
/>
</div>
<div v-if="payload.src === CUSTOM_MESSAGE_SRC.FROM_INPUT">
<MessageForm
:payload="payload"
@sendMessage="sendTextMessage"
/>
</div>
<div v-if="payload.src === CUSTOM_MESSAGE_SRC.PRODUCT_CARD">
<MessageProductCard :payload="payload" />
</div>
<div v-if="payload.src === CUSTOM_MESSAGE_SRC.RICH_TEXT">
<MessageRichText :payload="payload" />
</div>
<div v-if="payload.src === CUSTOM_MESSAGE_SRC.STREAM_TEXT">
<MessageStream :payload="payload" />
</div>
</div>
</template>
<script lang="ts">
import vue from '../adapter-vue';
import { JSONToObject } from '../utils/index';
import { CUSTOM_MESSAGE_SRC } from '../constant';
import { customerServicePayloadType, IMessageModel } from '../interface';
import MessageBranch from './message-branch.vue';
import MessageForm from './message-form/index.vue';
import MessageIMRobotWelcome from './message-robot-welcome.vue';
import MessageProductCard from './message-product-card.vue';
import MessageRichText from './message-rich-text.vue';
import MessageStream from './message-stream.vue';
const { computed } = vue;
interface Props {
message: IMessageModel;
}
export default {
components: {
MessageBranch,
MessageForm,
MessageProductCard,
MessageRichText,
MessageIMRobotWelcome,
MessageStream,
},
props: {
message: {
type: Object as () => IMessageModel,
default: () => ({}),
},
},
emits: ['sendMessage'],
setup(props: Props, { emit }) {
const payload = computed<customerServicePayloadType>(() => {
return props.message && JSONToObject(props.message?.payload?.data);
});
const sendTextMessage = (text: string) => {
emit('sendMessage', text);
};
return {
payload,
sendTextMessage,
CUSTOM_MESSAGE_SRC,
};
},
};
</script>

View File

@@ -0,0 +1,68 @@
<template>
<div class="form-branch-container">
<p
v-if="props.title"
class="card-title"
>
{{ props.title }}
</p>
<div
v-for="(item, index) in props.list"
:key="index"
class="form-branch-item"
@click="listItemClick(item)"
>
{{ item.content }}
</div>
</div>
</template>
<script lang="ts">
interface branchItem {
content: string;
desc: string;
}
interface Props {
title: string;
list: branchItem[];
}
export default {
props: {
title: {
type: String,
default: '',
},
list: {
type: Array,
default: () => [],
},
},
emits: ['input-click'],
setup(props: Props, { emit }) {
const listItemClick = (branch: branchItem): void => {
emit('input-click', branch);
};
return {
props,
listItemClick,
};
},
};
</script>
<style lang="scss">
.form-branch-container {
.card-title {
margin-bottom: 8px;
}
.form-branch-item {
font-weight: 400;
color: rgba(54, 141, 255, 1);
padding-top: 5px;
cursor: pointer;
padding-bottom: 5px;
}
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<div class="form-input-container">
<div class="card-title">
{{ props.title }}
</div>
<div class="form-input-box">
<input
v-model="text"
class="form-input"
>
<button
class="form-button"
:disabled="disabled"
@click="listItemClick"
/>
</div>
</div>
</template>
<script lang="ts">
import vue from '../../adapter-vue';
const { ref } = vue;
interface Props {
title: string;
}
export default {
props: {
title: {
type: String,
default: '',
},
},
emits: ['input-submit'],
setup(props: Props, { emit }) {
const disabled = ref<boolean>(false);
const text = ref<string>('');
const listItemClick = (): void => {
disabled.value = true;
emit('input-submit', text.value);
};
return {
disabled,
text,
listItemClick,
props,
};
},
};
</script>
<style lang="scss">
.form-input-container {
.card-title {
margin-bottom: 8px;
}
.form-input-box {
display: flex;
button:disabled {
background: #d8d8d8;
}
}
.form-input {
width: 100%;
height: 36px;
border-radius: 8px 0 0 8px;
border: 1px rgba(221, 221, 221, 1) solid;
}
.form-button {
position: relative;
height: 40px;
width: 42px;
font-size: 16px;
border-radius: 0 8px 8px 0;
border: 0 rgba(221, 221, 221, 1) solid;
background: #006eff;
color: white;
cursor: pointer;
}
.form-button::before {
content: "";
position: absolute;
width: 10px;
height: 10px;
top: 50%;
right: 40%;
border-left: 2px solid #fff;
border-bottom: 2px solid #fff;
transform: translate(0, -50%) rotate(-135deg);
}
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<div
v-if="content.type === 1"
class="message-form"
>
<FormBranch
:title="content.header"
:list="content.items"
@input-click="handleContentListItemClick"
/>
</div>
<div
v-else
class="message-form"
>
<FormInput
:title="content.header"
@input-submit="handleFormSaveInputSubmit"
/>
</div>
</template>
<script lang="ts">
import vue from '../../adapter-vue';
import FormBranch from './form-branch.vue';
import FormInput from './form-input.vue';
const { computed } = vue;
interface branchItem {
content: string;
desc: string;
}
interface Props {
payload: any;
}
export default {
components: {
FormBranch,
FormInput,
},
props: {
payload: {
type: Object,
default: () => ({}),
},
},
emits: ['sendMessage'],
setup(props: Props, { emit }) {
const content = computed(() => {
return props.payload?.content || {
type: 0,
header: '',
items: [],
};
});
const handleContentListItemClick = (branch: branchItem) => {
emit('sendMessage', { text: branch.content });
};
const handleFormSaveInputSubmit = (text: string) => {
emit('sendMessage', { text });
};
return {
content,
handleContentListItemClick,
handleFormSaveInputSubmit,
};
},
};
</script>
<style lang="scss">
.message-form {
max-width: 300px;
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div
class="message-product-card"
@click="jumpProductCard"
>
<image
v-if="isApp"
class="product-img"
:src="props.payload.content.pic"
/>
<img
v-else
class="product-img"
:src="props.payload.content.pic"
>
<div class="product-card-information">
<div class="product-card-title">
{{ props.payload.content.header }}
</div>
<div class="product-card-description">
{{ props.payload.content.desc }}
</div>
</div>
</div>
</template>
<script lang="ts">
import { customerServicePayloadType } from '../interface';
import { isApp } from '../utils/env';
// eslint-disable-next-line
declare var uni: any;
interface Props {
payload: customerServicePayloadType;
}
export default {
props: {
payload: {
type: Object as () => customerServicePayloadType,
default: () => ({}),
},
},
emits: ['sendMessage'],
setup(props: Props) {
const jumpProductCard = () => {
if (window) {
window.open(props.payload.content.url, '_blank');
} else {
uni && uni.navigateTo({ url: `/TUIKit/components/TUIChat/web-view?url=${props.payload.content.url}` });
}
};
return {
props,
isApp,
jumpProductCard,
};
},
};
</script>
<style lang="scss" scoped>
.message-product-card {
min-width: 224px;
max-width: 288px;
background: #fff;
border: 1px solid #ddd;
display: flex;
padding: 12px;
border-radius: 5px;
.product-img {
width: 86px;
height: 86px;
}
.product-card-information {
margin-left: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
.product-card-title {
font-size: 12px;
max-width: 165px;
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
word-break: break-all;
}
.product-card-description {
font-size: 16px;
max-width: 165px;
color: #ff6c2e;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<RatingStar
v-if="ratingTemplate.type === RATING_TEMPLATE_TYPE.STAR"
:ratingTemplate="ratingTemplate"
@sendMessage="sendCustomMessage"
/>
<RatingNumber
v-else
:ratingTemplate="ratingTemplate"
@sendMessage="sendCustomMessage"
/>
</template>
<script lang="ts">
import vue from '../../adapter-vue';
import { JSONToObject } from '../../utils/index';
import { RATING_TEMPLATE_TYPE } from '../../constant';
import RatingStar from './message-rating-star.vue';
import RatingNumber from './message-rating-number.vue';
import { IMessageModel } from '../../interface';
const { computed } = vue;
interface Props {
message: IMessageModel;
}
export default {
components: {
RatingStar,
RatingNumber,
},
props: {
message: {
type: Object as () => IMessageModel,
default: () => ({}),
},
},
emits: ['sendMessage'],
setup(props: Props, { emit }) {
const ratingTemplate = computed(() => {
const data = props.message && JSONToObject(props.message.payload.data);
return data?.menuContent;
});
const sendCustomMessage = (data: any) => {
emit('sendMessage', data);
};
return {
sendCustomMessage,
ratingTemplate,
RATING_TEMPLATE_TYPE,
};
},
};
</script>

View File

@@ -0,0 +1,248 @@
<template>
<div class="message-rating-star">
<p class="rating-head">
{{ props.ratingTemplate.head }}
</p>
<div class="rating-card">
<span class="card-title">请对本次服务进行评价</span>
<div class="card-wrapper">
<div style="max-width: 250px">
<div
v-for="(item, index) in numberList"
:key="index"
:class="{
'active': !(index !== selectValue && index !== hoverValue),
'de-active': index !== selectValue && index !== hoverValue,
}"
:style="{
marginLeft: index === 0 ? 0 + 'px' : 20 + 'px',
margin: 5 + 'px',
}"
@click="setValue(index)"
@mouseenter="setHoverValue(index)"
@mouseleave="setHoverValue(-1)"
>
{{ item + 1 }}
</div>
</div>
</div>
<div :style="{ marginTop: 10 + 'px', marginBottom: 10 + 'px' }">
{{
hoverValue === -1
? selectValue === -1
? "如果满意请给好评哦~"
: desc[selectValue]
: desc[hoverValue]
}}
</div>
<button
class="submit-button"
:disabled="hasReply || hasExpire"
@click="submitRatingStar"
>
提交评价
</button>
</div>
<p
v-if="hasReply"
class="rating-tail"
:style="{
marginTop: 20 + 'px',
}"
>
{{ props.ratingTemplate.tail }}
</p>
</div>
</template>
<script lang="ts">
import vue from '../../adapter-vue';
import { CUSTOM_MESSAGE_SRC } from '../../constant';
import { ratingTemplateType } from '../../interface';
const { computed, ref, watchEffect } = vue;
interface Props {
ratingTemplate: ratingTemplateType;
}
export default {
props: {
ratingTemplate: {
type: Object as () => ratingTemplateType,
default: () => ({}),
},
},
emits: ['sendMessage'],
setup(props: Props, { emit }) {
const hasReply = ref<boolean>(false);
const sessionId = ref<string>('');
const selectValue = ref<number>(-1);
const hoverValue = ref<number>(-1);
const hasExpire = ref<boolean>(false);
const desc = computed(() => {
return props.ratingTemplate?.menu.map(item => item.content);
});
const numberList = computed(() => {
return props.ratingTemplate?.menu.map((item, index) => index);
});
watchEffect(() => {
sessionId.value = props.ratingTemplate.sessionId || '';
if (props.ratingTemplate.selected != undefined) {
for (let i = 0; i < props.ratingTemplate.menu.length; i++) {
if (props.ratingTemplate.menu[i].id == props.ratingTemplate.selected.id) {
hasReply.value = true;
selectValue.value = i;
break;
}
}
}
const timestamp = Math.floor(new Date().getTime() / 1000);
if (timestamp > props.ratingTemplate.expireTime) {
hasExpire.value = true;
}
});
const setValue = (val: number) => {
if (!hasReply.value) {
selectValue.value = val;
}
};
const setHoverValue = (value: number) => {
if (!hasReply.value) {
hoverValue.value = value;
}
};
const submitRatingStar = () => {
if (selectValue.value >= 0) {
const submitData = {
data: JSON.stringify({
src: CUSTOM_MESSAGE_SRC.MENU_SELECTED,
menuSelected: {
id: props.ratingTemplate.menu[selectValue.value].id,
content: props.ratingTemplate.menu[selectValue.value].content,
sessionId: sessionId.value,
},
customerServicePlugin: 0,
}),
};
hasReply.value = true;
emit('sendMessage', submitData);
}
};
return {
props,
hasReply,
sessionId,
selectValue,
hoverValue,
hasExpire,
desc,
numberList,
setValue,
setHoverValue,
submitRatingStar,
};
},
};
</script>
<style lang="scss" scoped>
.rating-head {
font-size: 14px;
font-weight: 400;
color: #999;
}
.rating-tail {
font-size: 14px;
font-weight: 400;
color: #999;
}
.card-title {
font-size: 14px;
font-weight: 500;
}
.rating-card {
min-width: 270px;
width: 50%;
background: #fbfbfb;
border-radius: 20px;
border: 0;
margin-top: 10px;
padding-top: 20px;
padding-bottom: 20px;
button:disabled {
background: #d8d8d8;
}
}
.message-rating-star {
text-align: center;
display: flex;
flex-flow: column wrap;
justify-content: center;
padding-bottom: 30px;
align-items: center;
}
.card-wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
padding-top: 10px;
}
.submit-button {
width: 50%;
height: 50px;
background-color: #0365f9;
font-size: 18px;
font-weight: 400;
color: white;
border: 0;
border-radius: 8px;
cursor: pointer;
}
.de-active {
height: 34px;
width: 34px;
display: inline-block;
border: 0 solid #006eff0d;
border-radius: 5px;
color: #006eff;
font-weight: 400;
font-size: 16px;
text-align: center;
line-height: 34px;
background: #006eff0d;
}
.active {
width: 34px;
height: 34px;
display: inline-block;
background: linear-gradient(
136.96deg,
rgba(10, 124, 255, 0.3) -39.64%,
#0a7cff 131.39%
);
border-radius: 5px;
color: white;
font-weight: 400;
font-size: 16px;
border: 0 solid #0a7cff;
text-align: center;
line-height: 34px;
}
</style>

View File

@@ -0,0 +1,238 @@
<template>
<div class="message-rating-star">
<p class="rating-head">
{{ props.ratingTemplate.head }}
</p>
<div class="rating-card">
<span class="card-title">请对本次服务进行评价</span>
<div class="card-wrapper">
<div style="max-width: 200px">
<div
v-for="(item, index) in starList"
:key="index"
style="display: inline-block"
@click="setValue(index)"
@mouseenter="setHoverValue(index)"
@mouseleave="setHoverValue(-1)"
>
<Icon
v-if="item === 1"
:src="star"
width="30px"
height="30px"
/>
<Icon
v-else
:src="starLine"
width="30px"
height="30px"
/>
</div>
</div>
</div>
<div :style="{ marginTop: 10 + 'px', marginBottom: 10 + 'px' }">
{{
hoverValue === -1
? value === -1
? "如果满意请给好评哦~"
: desc[value]
: desc[hoverValue]
}}
</div>
<button
class="submit-button"
:disabled="hasReply || hasExpire"
@click="submitRatingStar"
>
提交评价
</button>
</div>
<p
v-if="hasReply"
class="rating-tail"
:style="{
marginTop: 20 + 'px',
}"
>
{{ props.ratingTemplate.tail }}
</p>
</div>
</template>
<script lang="ts">
import vue from '../../adapter-vue';
import { CUSTOM_MESSAGE_SRC } from '../../constant';
import { ratingTemplateType } from '../../interface';
import star from '../../assets/star.png';
import starLine from '../../assets/starLine.png';
import Icon from '../customer-icon.vue';
const { computed, ref, watchEffect } = vue;
interface Props {
ratingTemplate: ratingTemplateType;
}
export default {
components: {
Icon,
},
props: {
ratingTemplate: {
type: Object as () => ratingTemplateType,
default: () => ({}),
},
},
emits: ['sendMessage'],
setup(props: Props, { emit }) {
const hasReply = ref<boolean>(false);
const sessionId = ref<string>('');
const value = ref<number>(-1);
const hoverValue = ref<number>(-1);
const hasExpire = ref<boolean>(false);
watchEffect(() => {
sessionId.value = props.ratingTemplate.sessionId || '';
if (props.ratingTemplate.selected != undefined) {
for (let i = 0; i < props.ratingTemplate.menu.length; i++) {
if (props.ratingTemplate.menu[i].id == props.ratingTemplate.selected.id) {
hasReply.value = true;
value.value = i;
break;
}
}
}
const timestamp = Math.floor(new Date().getTime() / 1000);
if (timestamp > props.ratingTemplate.expireTime) {
hasExpire.value = true;
}
});
const desc = computed(() => {
return props.ratingTemplate?.menu.map((item) => {
return item.content;
});
});
const starList = computed(() => {
return props.ratingTemplate?.menu.map((item, index) => {
if (hoverValue.value !== -1) {
return index <= hoverValue.value ? 1 : 0;
} else {
return index <= value.value ? 1 : 0;
}
});
});
const setValue = (val: number) => {
if (hasReply.value) {
return;
}
value.value = val;
};
const setHoverValue = (value: number) => {
if (hasReply.value) {
return;
}
hoverValue.value = value;
};
const submitRatingStar = async () => {
if (value.value < 0) {
return;
}
const submitData = {
data: JSON.stringify({
src: CUSTOM_MESSAGE_SRC.MENU_SELECTED,
menuSelected: {
id: props.ratingTemplate.menu[value.value].id,
content: props.ratingTemplate.menu[value.value].content,
sessionId: sessionId.value,
},
customerServicePlugin: 0,
}),
};
hasReply.value = true;
emit('sendMessage', submitData);
};
return {
props,
hasReply,
sessionId,
value,
hoverValue,
hasExpire,
desc,
starList,
setValue,
setHoverValue,
submitRatingStar,
star,
starLine,
};
},
};
</script>
<style lang="scss" scoped>
.rating-head {
font-size: 14px;
font-weight: 400;
color: #999;
}
.rating-tail {
font-size: 14px;
font-weight: 400;
color: #999;
}
.card-title {
font-size: 14px;
font-weight: 500;
}
.rating-card {
min-width: 270px;
width: 50%;
background: #fbfbfb;
border-radius: 20px;
border: 0;
margin-top: 10px;
padding-top: 20px;
padding-bottom: 20px;
button:disabled {
background: #d8d8d8;
}
}
.message-rating-star {
text-align: center;
display: flex;
flex-flow: column wrap;
justify-content: center;
padding-bottom: 30px;
align-items: center;
}
.card-wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
padding-top: 10px;
}
.submit-button {
width: 50%;
height: 50px;
background-color: #0365f9;
font-size: 18px;
font-weight: 400;
color: white;
border: 0;
border-radius: 8px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<div
class="rich-text"
v-html="formatedContent"
/>
</template>
<script lang="ts">
import vue from '../adapter-vue';
import { marked } from 'marked';
import { customerServicePayloadType } from '../interface';
const { computed } = vue;
interface Props {
payload: customerServicePayloadType;
}
export default {
props: {
payload: {
type: Object as () => customerServicePayloadType,
default: () => ({}),
},
},
setup(props: Props) {
const formatedContent = computed(() => {
let richtext = marked.parse(props.payload.content);
const regex = new RegExp('<img', 'gi');
richtext = richtext.replace(regex, `<img style="max-width: 100%;"`);
return richtext;
});
return {
props,
formatedContent,
};
},
};
</script>
<style lang="scss">
.rich-text {
div,
ul,
ol,
dt,
dd,
li,
dl,
h1,
h2,
h3,
h4,
p,
img,
a {
max-width: 100%;
}
a {
color: blue;
}
}
</style>

View File

@@ -0,0 +1,164 @@
<template>
<div class="welcome-card">
<div class="welcome-title">
<div class="welcome-title-left-container">
<Icon :src="imRobotGuess" />
<p
v-if="title"
class="card-title"
>
{{ title }}
</p>
</div>
<div
class="change-wrapper"
@click="changeBranchList()"
>
<Icon :src="refresh" />
</div>
</div>
<div
v-for="(item, index) in showList"
:key="index"
class="welcome-item"
@click="handleContentListItemClick(item)"
>
<div>{{ item.content }}</div>
<Icon :src="iconRight" />
</div>
</div>
</template>
<script lang="ts">
import vue from '../adapter-vue';
import imRobotGuess from '../assets/imRobotGuess.svg';
import refresh from '../assets/refresh.svg';
import iconRight from '../assets/iconRight.svg';
import Icon from './customer-icon.vue';
import { customerServicePayloadType } from '../interface';
const { reactive, toRefs } = vue;
interface Props {
payload: customerServicePayloadType;
}
interface welcomeBranchItem {
id: string;
content: string;
answer: string;
}
export default {
components: {
Icon,
},
props: {
payload: {
type: Object as () => customerServicePayloadType,
default: () => ({ content: { title: '', items: [] } }),
},
},
emits: ['sendMessage'],
setup(props: Props, { emit }) {
const data = reactive({
// title
title: props.payload?.content?.title || '',
// all branch list
list: props.payload?.content?.items || [],
// current branch list
showList: (props.payload?.content?.items || []).slice(0, 5),
// current page number
pageNumber: 1,
});
const handleContentListItemClick = (branch: welcomeBranchItem) => {
emit('sendMessage', { text: branch.content });
};
const changeBranchList = () => {
if (data.pageNumber * 5 >= data.list?.length) {
data.pageNumber = 0;
}
data.showList = data.list?.slice(
data.pageNumber * 5,
data.pageNumber * 5 + 5,
);
data.pageNumber += 1;
};
return {
...toRefs(data),
handleContentListItemClick,
imRobotGuess,
refresh,
iconRight,
changeBranchList,
};
},
};
</script>
<style lang="scss">
.welcome-card {
min-width: 250px;
max-width: 350px;
.welcome-title {
display: flex;
height: 40px;
justify-content: space-between;
align-items: center;
}
.welcome-title-left-container {
display: flex;
align-items: center;
}
.card-title {
display: inline-block;
margin-left: 8px;
font-size: 16px;
}
.el-link {
display: block;
font-weight: 400;
padding-top: 5px;
padding-bottom: 5px;
}
/* stylelint-disable */
.el-link__inner {
display: flex;
justify-content: space-between;
align-items: center;
}
/* stylelint-enable */
.branch-number {
margin-left: 15px;
margin-right: 15px;
font-size: 20px;
display: inline-block;
}
.change-wrapper {
cursor: pointer;
}
.welcome-item {
padding: 6px;
color: #999;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.welcome-item:hover {
background: #f2f7ff;
}
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="message-stream">
{{ displayedContent }}<span v-if="!isFinished" class="blinking-cursor" />
</div>
</template>
<script lang="ts">
import vue from "../adapter-vue";
import { customerServicePayloadType } from "../interface";
const { ref, watchEffect, onBeforeUnmount, onMounted } = vue;
interface Props {
payload: customerServicePayloadType;
}
export default {
props: {
payload: {
type: Object as () => customerServicePayloadType,
default: () => ({}),
},
},
setup(props: Props) {
const content = ref<string>("");
const displayedContent = ref<string>("");
const isFinished = ref<boolean>(false);
let intervalId: number | null = null;
let currentIndex = 0;
const updateDisplayedContent = () => {
if (intervalId) {
window.clearInterval(intervalId);
}
intervalId = window.setInterval(() => {
if (currentIndex < content.value.length) {
displayedContent.value += content.value[currentIndex];
currentIndex++;
} else {
window.clearInterval(intervalId!);
intervalId = null;
}
}, 50);
};
onMounted(() => {
content.value = props?.payload?.chunks?.join("") ?? "";
displayedContent.value = content.value;
currentIndex = content.value.length;
});
watchEffect(() => {
const newContent = props?.payload?.chunks?.join("") ?? "";
if (newContent.length > currentIndex) {
content.value = newContent;
updateDisplayedContent();
}
});
watchEffect(() => {
isFinished.value = props?.payload?.isFinished === 1;
});
onBeforeUnmount(() => {
if (intervalId) {
window.clearInterval(intervalId);
}
});
return {
content,
props,
isFinished,
displayedContent,
};
},
};
</script>
<style lang="scss" scoped>
.message-stream {
word-break: break-all;
font-size: 14px;
.blinking-cursor {
display: inline-block;
width: 1px;
height: 16px;
background-color: black;
animation: blink 1s step-end infinite;
vertical-align: sub;
}
@keyframes blink {
0%,
100% {
background-color: transparent;
}
50% {
background-color: black;
}
}
}
</style>

View File

@@ -0,0 +1,114 @@
// https://cloud.tencent.com/document/product/269/96058
export const CUSTOM_MESSAGE_SRC = {
OFFICIAL_ACCOUNT: '1',
MINI_APP: '2',
MINI_APP_SERVICE_ACCOUNT: '3',
BACKEND_INTERNAL: '4',
WEB: '5',
SESSION_MESSAGE_SLICE: '6',
MINI_APP_AUTO: '7',
INTERNAL: '8',
MENU: '9',
MENU_SELECTED: '10',
CLIENT_STATE: '11',
TYPING_STATE: '12',
ROBOT: '13',
BRANCH: '15',
BRANCH_NUMBER: 15,
MEMBER: '17',
NO_SEAT_ONLINE: '18',
END: '19',
TIMEOUT: '20',
FROM_INPUT: '21',
PRODUCT_CARD: '22',
SATISFACTION_CON: '23',
USER_SATISFACTION: '24',
ROBOT_MSG: '29',
RICH_TEXT: '30',
STREAM_TEXT: '31',
};
// im message extra type
export const IM_MESSAGE_EXTRA_TYPE = {
INFO: 'INFO',
ROBERT_REPLAY_PLACEHOLDER: 'ROBOT_REPLAY_PLACEHOLDER',
};
// rating template type
export const RATING_TEMPLATE_TYPE = {
STAR: 1,
NUMBER: 2,
};
// rating state
export const RATING_STATE = {
NONE: 1,
IN_PROGRESS: 2,
DONE: 3,
};
// rating send rule
export const RATING_SEND_RULE = {
ALLOW_AUTO_SEND: 1,
ALLOW_SERVICE_SEND: 2,
ALLOW_CLIENT_SEND: 4,
};
// send rating error code
export const SEND_RATING_ERROR_CODE = {
SESSION_EXPIRED_OR_NOT_START: 10150,
ACCESS_DATA_ERROR: 10151,
DUPLICATE_SUBMIT: 10152,
INTERNAL_ERROR: 10153,
NO_STAFF: 10154,
};
// clent state
export const CLIENT_STATE = {
ONLINE: '1',
OFFLINE: '2',
};
// IM message type
export const IM_TYPE = {
WEB: 'web',
H5: 'h5',
};
// IM message status
export const IM_STATUS = {
SUCCESS: 'success',
FAIL: 'fail',
UN_SEND: 'unSend',
READ: 'read',
};
// robot command
export const ROBOT_COMMAND = {
UPDATE_BUBBLE: 'updateBubble',
UPDATE_SEARCH_TIPS: 'updateSearchTips',
SHOW_DIALOG: 'showDialog',
FEEDBACK: 'feedback',
SELECT_RECOMMEND: 'selectRecommend',
SELECT_SEARCH_TIP: 'selectSearchTips',
UPDATE_BOT_STATUS: 'updateBotStatus',
};
// robot message type
export const ROBOT_MESSAGE_TYPE = {
SIMPLE_TEXT: 'simpleText',
RICH_TEXT: 'richText',
MULTI_LINE_TEXT: 'multiLineText',
CANDIDATE_ANSWER: 'candidateAnswer',
QUESTION_LIST: 'questionList',
};
// robot status
export const ROBOT_STATUS = {
IN: 'inBot',
LEAVE: 'leaveBot',
};
// message type
export const TYPES = {
MSG_CUSTOM: 'TIMCustomElem',
};

View File

@@ -0,0 +1,18 @@
import TUICustomerPluginServer from './server';
import { isMessageInvisible } from './utils/index';
const TUICustomerServer = TUICustomerPluginServer.getInstance();
// 判断消息是否为客服号的自定义消息
const isCustomerServicePluginMessage = TUICustomerServer.isCustomerServicePluginMessage.bind(TUICustomerServer);
// 设置客服号
const setCustomerServiceAccounts = TUICustomerServer.setCustomerServiceAccounts.bind(TUICustomerServer);
// 获取客服号
const getCustomerServiceAccounts = TUICustomerServer.getCustomerServiceAccounts.bind(TUICustomerServer);
export {
isCustomerServicePluginMessage,
isMessageInvisible,
setCustomerServiceAccounts,
getCustomerServiceAccounts,
};

View File

@@ -0,0 +1,60 @@
<template>
<div class="message-custom">
<MessageRating
v-if="isMessageRating(props.message)"
:message="props.message"
@sendMessage="sendCustomMessage"
/>
<MessageCustomerService
v-else-if="isCustomerServiceMessage(props.message)"
:message="props.message"
@sendMessage="sendTextMessage"
/>
</div>
</template>
<script lang="ts">
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { isCustomerServiceMessage, isMessageRating } from './utils/index';
import { CustomMessagePayload, TextMessagePayload, IMessageModel } from './interface';
import MessageCustomerService from './components/message-customer-service.vue';
import MessageRating from './components/message-rating/index.vue';
interface Props {
message: IMessageModel;
}
export default {
components: {
MessageCustomerService,
MessageRating,
},
props: {
message: {
type: Object as () => IMessageModel,
default: () => ({}),
},
},
setup(props: Props) {
const sendTextMessage = (payload: TextMessagePayload) => {
TUICore.callService({
serviceName: TUIConstants.TUIChat.SERVICE.NAME,
method: TUIConstants.TUIChat.SERVICE.METHOD.SEND_TEXT_MESSAGE,
params: { payload },
});
};
const sendCustomMessage = (payload: CustomMessagePayload) => {
TUICore.callService({
serviceName: TUIConstants.TUIChat.SERVICE.NAME,
method: TUIConstants.TUIChat.SERVICE.METHOD.SEND_CUSTOM_MESSAGE,
params: { payload },
});
};
return {
props,
sendTextMessage,
sendCustomMessage,
isCustomerServiceMessage,
isMessageRating,
};
},
};
</script>

View File

@@ -0,0 +1,75 @@
export interface customerServicePayloadType {
chatbotPlugin?: number | string;
customerServicePlugin?: number | string;
src: string | number;
content: any;
subtype?: string;
isFinished?: number;
chunks?: string[];
}
interface IMenuItem {
content: string;
id: string;
}
export interface ratingTemplateType {
allowClientSendRating: boolean;
effectiveHour: number;
head: string;
tail: string;
type: number;
menu: IMenuItem[];
expireTime: number;
selected?: IMenuItem;
sessionId?: string;
}
export interface TextMessagePayload {
text: string;
}
export interface CustomMessagePayload {
data: string;
description: string;
extension: string;
}
export interface IMessageModel {
ID: string;
type: string;
payload: any;
conversationID: string;
conversationType: string;
to: string;
from: string;
flow: string;
time: number;
status: string;
isRevoked: boolean;
priority: string;
nick: string;
avatar: string;
isPeerRead: boolean;
nameCard: string;
atUserList: string[];
cloudCustomData: string;
isDeleted: boolean;
isModified: boolean;
needReadReceipt: boolean;
readReceiptInfo: any;
isBroadcastMessage: boolean;
isSupportExtension: boolean;
receiverList?: string[];
revoker: string;
sequence: number;
progress: number;
revokerInfo: {
userID: string;
nick: string;
avatar: string;
};
revokeReason: string;
hasRiskContent: boolean;
[key: string]: any;
}

View File

@@ -0,0 +1,35 @@
{
"name": "@tencentcloud/tui-customer-service-plugin",
"version": "2.2.6",
"description": "chat uikit tui-customer-service-plugin",
"main": "index",
"keywords": [
"uikit",
"chat",
"uni-app",
"IM",
"tencent",
"tencentcloud",
"messaging",
"即时通信",
"通信",
"WebSocket"
],
"dependencies": {
"@tencentcloud/universal-api": "latest",
"@vue/composition-api": "^1.0.0-rc.1",
"marked": "4.0.0"
},
"peerDependencies": {
"@tencentcloud/tui-core": "latest",
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^2.0.0 || >=3.0.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
},
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,89 @@
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import {
isCustomerServiceMessage,
isMessageInvisible,
} from './utils/index';
import { IMessageModel } from './interface';
export default class TUICustomerServer {
static instance: TUICustomerServer;
private customerServiceAccounts: any[];
constructor() {
console.log('TUICustomerServer.init ok');
TUICore.registerService(TUIConstants.TUICustomerServicePlugin.SERVICE.NAME, this);
TUICore.registerExtension(TUIConstants.TUIContact.EXTENSION.CONTACT_LIST.EXT_ID, this);
this.customerServiceAccounts = ['@customer_service_account'];
}
static getInstance(): TUICustomerServer {
if (!TUICustomerServer.instance) {
TUICustomerServer.instance = new TUICustomerServer();
}
return TUICustomerServer.instance;
}
// Set customer service number
public setCustomerServiceAccounts(accounts: any[]) {
this.customerServiceAccounts = accounts;
}
// Obtain customer service number
public getCustomerServiceAccounts() {
return this.customerServiceAccounts;
}
// Determine if the current session is a customer service session
private isCustomerConversation(conversationID: string) {
const userID = (conversationID && conversationID.slice(3)) || '';
return this.customerServiceAccounts.indexOf(userID) > -1;
}
// Determine if the current message is a customer service message
public isCustomerServicePluginMessage(message: IMessageModel) {
if (!message || !this.isCustomerConversation(message.conversationID)) {
return false;
}
return isCustomerServiceMessage(message) || isMessageInvisible(message);
}
public onGetExtension(extensionID: string) {
if (extensionID === TUIConstants.TUIContact.EXTENSION.CONTACT_LIST.EXT_ID) {
return [
{
weight: 0,
icon: '',
text: '客服号',
data: {
name: 'customer',
accountList: this.customerServiceAccounts,
},
},
];
}
}
public onCall(method: string, params: any) {
switch (method) {
case TUIConstants.TUICustomerServicePlugin.SERVICE.METHOD.ACTIVE_CONVERSATION:
if (this.isCustomerConversation(params.conversationID)) {
TUICore.callService({
serviceName: TUIConstants.TUIChat.SERVICE.NAME,
method: TUIConstants.TUIChat.SERVICE.METHOD.SET_CHAT_TYPE,
params: { chatType: 'customerService' },
});
TUICore.callService({
serviceName: TUIConstants.TUIChat.SERVICE.NAME,
method: TUIConstants.TUIChat.SERVICE.METHOD.SEND_CUSTOM_MESSAGE,
params: {
to: params.conversationID.slice(3),
conversationType: 'C2C',
payload: {
data: JSON.stringify({ src: '7', customerServicePlugin: 0 }),
},
},
});
}
break;
}
}
}

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
// Type Checking
"strict": true,
"exactOptionalPropertyTypes": true,
"noImplicitAny": false,
// Language and Environment
"target": "ESNext",
// Modules
"moduleResolution": "node",
// Completeness
"skipLibCheck": true,
// Interop Constraints
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
// Emit
"sourceMap": true,
// "strictFunctionTypes": false,
"isolatedModules": false,
"baseUrl": ".",
"paths": {
"@/*": ["components/*"],
},
"lib": ["esnext", "dom"]
},
"include": [
"**/*.js",
"**/*.d.ts",
"**/*.ts",
"**/*.vue",
"typings"
],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,20 @@
/* eslint-disable */
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
declare module "*.svg";
declare module "*.png";
declare module "*.jpg";
declare module "*.jpeg";
declare module "*.gif";
declare module "*.bmp";
declare module "*.tiff";
declare module "*.json" {
const content: any;
export default content;
}

View File

@@ -0,0 +1,15 @@
import { getPlatform } from '@tencentcloud/universal-api';
declare const uni: any;
export const isPC = getPlatform() === 'pc';
export const isH5 = getPlatform() === 'h5';
export const isWeChat = getPlatform() === 'wechat';
export const isApp = getPlatform() === 'app';
export const isUniFrameWork = typeof uni !== 'undefined';
// H5、小程序、app 均认为是手机端产品,如果需要统一手机端 UI 样式,可以直接用 isMobile 控制
export const isMobile = isH5 || isWeChat || isApp;

View File

@@ -0,0 +1,56 @@
import { customerServicePayloadType, IMessageModel } from '../interface';
import { CUSTOM_MESSAGE_SRC, TYPES } from '../constant';
// Determine if it is a JSON string
export function isJSON(str: string): boolean {
// eslint-disable-next-line no-useless-escape
if (typeof str === 'string') {
try {
const data = JSON.parse(str);
if (data) {
return true;
}
return false;
} catch (error: any) {
return false;
}
}
return false;
}
// Determine if it is a JSON string
export function JSONToObject(str: string) {
if (!isJSON(str)) {
return str;
}
return JSON.parse(str);
}
export function isCustomerServiceMessage(message: IMessageModel): boolean {
const customerServicePayload: customerServicePayloadType = JSONToObject(message?.payload?.data);
return Number(customerServicePayload?.customerServicePlugin) === 0 || Number(customerServicePayload?.chatbotPlugin) === 1;
}
export const isMessageRating = (message: IMessageModel): boolean => {
const customerServicePayload: customerServicePayloadType = JSONToObject(message?.payload?.data);
return isCustomerServiceMessage(message) && customerServicePayload.src === CUSTOM_MESSAGE_SRC.MENU;
};
export const isMessageInvisible = (message: IMessageModel): boolean => {
const customerServicePayload: customerServicePayloadType = JSONToObject(message?.payload?.data);
const robotCommandArray = ['feedback', 'updateBotStatus'];
const whiteList = [
CUSTOM_MESSAGE_SRC.MENU,
CUSTOM_MESSAGE_SRC.BRANCH,
CUSTOM_MESSAGE_SRC.BRANCH_NUMBER,
CUSTOM_MESSAGE_SRC.FROM_INPUT,
CUSTOM_MESSAGE_SRC.PRODUCT_CARD,
CUSTOM_MESSAGE_SRC.ROBOT_MSG,
CUSTOM_MESSAGE_SRC.RICH_TEXT,
CUSTOM_MESSAGE_SRC.STREAM_TEXT,
];
const isCustomerMessage = message?.type === TYPES.MSG_CUSTOM;
const isCustomerInvisible = customerServicePayload?.src && !whiteList.includes(customerServicePayload?.src);
const isRobot = customerServicePayload?.src === CUSTOM_MESSAGE_SRC.ROBOT && robotCommandArray.indexOf(customerServicePayload?.content?.command) !== -1;
return isCustomerMessage && (isCustomerInvisible || isRobot);
};