消息
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user