消息
This commit is contained in:
40
TUIKit/tui-customer-service-plugin/.stylelintrc.json
Normal file
40
TUIKit/tui-customer-service-plugin/.stylelintrc.json
Normal 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"
|
||||
}
|
||||
}
|
||||
50
TUIKit/tui-customer-service-plugin/CHANGELOG.md
Normal file
50
TUIKit/tui-customer-service-plugin/CHANGELOG.md
Normal 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
|
||||
|
||||
- 支持全平台客服插件
|
||||
36
TUIKit/tui-customer-service-plugin/README.md
Normal file
36
TUIKit/tui-customer-service-plugin/README.md
Normal 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)
|
||||
11
TUIKit/tui-customer-service-plugin/adapter-vue-uniapp.ts
Normal file
11
TUIKit/tui-customer-service-plugin/adapter-vue-uniapp.ts
Normal 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 };
|
||||
73
TUIKit/tui-customer-service-plugin/adapter-vue-web.ts
Normal file
73
TUIKit/tui-customer-service-plugin/adapter-vue-web.ts
Normal 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 };
|
||||
11
TUIKit/tui-customer-service-plugin/adapter-vue.ts
Normal file
11
TUIKit/tui-customer-service-plugin/adapter-vue.ts
Normal 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;
|
||||
3
TUIKit/tui-customer-service-plugin/assets/iconRight.svg
Normal file
3
TUIKit/tui-customer-service-plugin/assets/iconRight.svg
Normal 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 |
@@ -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 |
4
TUIKit/tui-customer-service-plugin/assets/refresh.svg
Normal file
4
TUIKit/tui-customer-service-plugin/assets/refresh.svg
Normal 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 |
BIN
TUIKit/tui-customer-service-plugin/assets/star.png
Normal file
BIN
TUIKit/tui-customer-service-plugin/assets/star.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
TUIKit/tui-customer-service-plugin/assets/starLine.png
Normal file
BIN
TUIKit/tui-customer-service-plugin/assets/starLine.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 512 B |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
104
TUIKit/tui-customer-service-plugin/components/message-stream.vue
Normal file
104
TUIKit/tui-customer-service-plugin/components/message-stream.vue
Normal 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>
|
||||
114
TUIKit/tui-customer-service-plugin/constant.ts
Normal file
114
TUIKit/tui-customer-service-plugin/constant.ts
Normal 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',
|
||||
};
|
||||
18
TUIKit/tui-customer-service-plugin/index.ts
Normal file
18
TUIKit/tui-customer-service-plugin/index.ts
Normal 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,
|
||||
};
|
||||
60
TUIKit/tui-customer-service-plugin/index.vue
Normal file
60
TUIKit/tui-customer-service-plugin/index.vue
Normal 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>
|
||||
75
TUIKit/tui-customer-service-plugin/interface.ts
Normal file
75
TUIKit/tui-customer-service-plugin/interface.ts
Normal 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;
|
||||
}
|
||||
35
TUIKit/tui-customer-service-plugin/package.json
Normal file
35
TUIKit/tui-customer-service-plugin/package.json
Normal 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"
|
||||
}
|
||||
89
TUIKit/tui-customer-service-plugin/server.ts
Normal file
89
TUIKit/tui-customer-service-plugin/server.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
TUIKit/tui-customer-service-plugin/tsconfig.json
Normal file
34
TUIKit/tui-customer-service-plugin/tsconfig.json
Normal 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"]
|
||||
}
|
||||
20
TUIKit/tui-customer-service-plugin/typings.d.ts
vendored
Normal file
20
TUIKit/tui-customer-service-plugin/typings.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
||||
15
TUIKit/tui-customer-service-plugin/utils/env.ts
Normal file
15
TUIKit/tui-customer-service-plugin/utils/env.ts
Normal 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;
|
||||
56
TUIKit/tui-customer-service-plugin/utils/index.ts
Normal file
56
TUIKit/tui-customer-service-plugin/utils/index.ts
Normal 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);
|
||||
};
|
||||
Reference in New Issue
Block a user