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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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